From 0d9032224d1c8aebe39c41142a0d13cd28c6ac8a Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 25 Jul 2026 14:28:08 +0200 Subject: [PATCH 1/5] feat(login): bake TREK's coastlines into a dot grid for the sign-in screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login screen is unauthenticated, so it cannot call the Atlas endpoint the rest of the app uses for country geometry. This is that same admin-0 bundle, sampled along the coastlines onto a 300x150 grid and delta-encoded — 15 KB instead of a 28 MB fetch nobody is allowed to make yet. --- client/src/pages/login/worldDots.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 client/src/pages/login/worldDots.ts diff --git a/client/src/pages/login/worldDots.ts b/client/src/pages/login/worldDots.ts new file mode 100644 index 000000000..4e542e7d7 --- /dev/null +++ b/client/src/pages/login/worldDots.ts @@ -0,0 +1,23 @@ +// Generated from server/assets/atlas/admin0.geojson.gz — TREK's own Atlas bundle. +// The login screen is unauthenticated and cannot fetch Atlas, so the coastlines ship +// as a 300x150 dot grid: sorted cell indices, delta-encoded in base36. +// Regenerate with the script in the design notes if the bundle ever changes. + +export const WORLD_GRID_W = 300; +export const WORLD_GRID_H = 150; +export const WORLD_LAT_TOP = 80; +export const WORLD_LAT_BOT = -58; + +const PACKED = + '1u.1.1.1.1.1.6.1.1.2.1.1.2.1.1.1.1.1.1.4.1.10.1.1.1.n.1.1.1.1.1.1.1.1.1.1.3.1.j.1.6.1.e.1.b.1.1.1.3.1.1.3g.1.4.1.1.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.1.10.1.1.1.n.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.12.1.b.1.1.1.1.1.1.1.1.1.1.1.1.39.1.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.3.1.1.1.1.1.1.1.1.2.11.1.1.1.1.o.1.1.1.1.1.1.1.1.1.1.1.1l.5.1.1.1.1.1.1.1.32.1.1.1.1.1.1.1.1.1.1.2.1.1.2.1.3.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.4.1.1.1.1.1.1.10.1.1.1.1.q.1.1.1.1.2.1.1.1.y.1.1.1.b.5.1.5.1.3.1.1.1.1.1.1.1.1.1.w.21.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.6.1.1.1.1.1.1.1.1.1.v.1.1.1.t.1.6.1.r.1.1.1.1.1.1.1.1.1.a.1.1.8.1.1.1.1.1.1.1.4.1.1.1.1.1.1.i.2.1.1.1.1.1.4.1.23.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.g.1.1.t.1.1.1.1.1p.1.1.1.1.1.1.1.1.d.1.3.1.1.1.1.1.1.2.1.d.1.i.1.1.1.1.1.1.1.1.1.1.1.1.1.1z.1.1.1.1.1.1.1.1.1.1.1.1.1.8.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.i.1.s.1.1.1.v.1.t.1.1.1.1.1.f.1.2.1.1.1.1.i.1.1.1.1.2.g.1.1.1.1.1.1.1.3.1.1.1.1z.1.5.1.1.1.5.1.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.1.1.g.1.1.m.1.1.1.1.1.1.1.1p.1.1.1.1.a.1.2.1.1.2.1.1.1.1.1.1.1.f.1.1.1.1.1.1.1.1.1.1.1.1.3.1.1.1.1.1.5.1.3.1.1.1.24.1.4.1.1.1.1.1.1.1.1.1.1.1.1.2.1.1.2.1.1.1.1.1.2.2.1.1.1.2.1.1.1.1.1.g.1.1.m.1.1.1.1.1p.1.1.1.1.b.1.1.1.1.1.1.1.1.1.1.1.j.1.b.1.1.1.4.1.8.1.1.1.1.1.1.1.1.1.q.1.1.h.1.p.1.1.1.1.1.1.1.1.1.5.1.2.1.3.1.1.1.1.1.1.3.1.1.1.1.1.2.1.1.1.1.1.1.1.1.d.1.1.1.1.j.1.1.1.1.1.1.1o.1.1.1.1.9.1.1.2.1.1.1.1.1.1.1.1.1.1.11.1.2.1.1.1.1.1.1.1.4.1.2.1.1.1.m.1.1.1.1.d.1.1.1.1.1.1.1.1.1.1.1.1.g.1.3.1.3.1.1.1.1.1.1.6.1.1.1.3.1.2.1.2.1.1.1.1.2.2.1.3.1.1.1.1.1.1.b.1.1.1.1.h.1.1.1.1.1.1.1.a.1.n.1.1.1.1.1.1.1.1.1.i.1.1.1.1.1.1.6.5.1.2.1.1.3.1.12.1.1.5.b.1.1.1.1.1.1.1.1.1.e.1.f.1.1.1.a.1.1.1.1.1.1.1.3.1.1.1.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.1.4.3.1.1.2.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.4.1.1.1.1.a.1.1.1.1.i.1.1.1.1.1.x.1.1.1.1.1.1.1.1.1.1.1.1.1.1.c.1.7.1.1.1.1.1.2.1.4.1.7.1.1r.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.2.1.a.1.1.1.i.2.1.1.1.1.3.4.1.1.3.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.1.2.1.2.1.1.1.1.4.1.1.b.1.1.1.e.1.2.1.1.1.1.w.1.1.1.1.1.1.1.1.1.1.2.1.3.1.1.1.1.4.1.1.1.2.1.1.1.1.1.1.1.1.1.1.3.1.1.1.1.3.1.1.1.1.1x.1.5.1.1.1.6.1.1.1.1.1.1.7.1.1.j.l.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.3.1.1.1.1.3.1.2.1.1.1.1.1.4.1.1.1.1.8.1.1.1.e.1.1.1.1.y.1.1.1.1.1.1.2.1.4.1.7.1.1.1.2.1.1.1.1.1.1.1.1.1.4.7.1.3.1.1.2.1.2c.1.4.1.1.1.6.1.1.1.g.r.1.1.8.1.7.1.1.1.1.1.3.1.1.2.1.3.1.2.1.1.1.6.1.1.1.d.1.12.1.1.1.6.5.2.1.1.1.4.1.1.1.1.1.1.i.1.1.1.1.4.2c.1.1.3.1.1.1.1.1.1.1.1.1.1.1.1.g.s.1.d.1.1.1.1.1.1.1.7.1.4.1.1.1.1.1.1.6.1.1.1.9.1.1.1.1.8.1.1.1.1.1.1.1.1.1.l.1.1.1.6.1.1.4.1.2.1.1.1.1.1.1.1.1.1.l.1.1.1.2h.1.1.1.1.1.2.1.1.2.1.1.3.1.g.17.1.1.1.1.1.1.4.1.1.1.5.1.1.1.1.1.7.1.1.1.7.1.1.1.1.a.1.1.1.1.4.1.k.1.1.1.1.5.1.2.1.3.4.1.1.1.1.1.34.1.1.1.1.5.1.1.5.1.1.1.1.1.g.13.1.1.2.1.1.1.2.1.1.1.1.1.1.1.1.1.1.4.1.1.a.1.1.7.1.f.1.3.1.1.1.j.1.1.1.1.6.1.1.1.4.1.3.1.1.1.1.1.34.1.1.1.9.1.1.4.1.1.1.g.15.1.1.2.1.1.1.1.1.1.1.1.1.3.1.1.1.1.1.1.1.b.1.6.1.1.g.1.1.1.1.j.1.1.1.1.1.4.1.1.1.1.1.5.1.1.5.39.1.d.1.j.14.1.1.6.1.2.1.1.1.1.1.1.1.1.1.1.1.1.1.c.1.5.1.s.1.a.1.1.1.3.4.1.2.8.1.2z.2.1.1.8.1.1.1.1.c.1.1.b.1.1.1.1.4.13.1.b.1.1.4.1.1.1.2.1.1.c.1.1.1.1.1.1.1.t.9.1.1.1.4.4.3.1.5.1.1.2w.1.1.1.1.1.1.1.6.1.1.1.b.4.1.1.1.1.1.7.1.1.1.1.1.1.1.1.1.1.1.1.11.d.1.6.1.1.2.1.d.1.1.1.1.1.y.1.4.1.1.5.4.1.1.1.1.1.1.1.1.1.1.2w.1.3.1.1.1.1.1.1.1.1.1.1.1.i.1.1.1.1.6.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.x.c.2.6.1.3.1.1.f.1.1.x.1.1.5.1.3.1.1.4.1.1.1.1.1.1.1.1.1.1.1.2l.1.1.1.1.1.1.1.1.1.1.4.1.1.1.1.1.1.3.o.1.1.1.1.2.1.1.1.a.1.1.1.1.1.w.1.c.1.6.1.1.1.1.1.1.1.1a.1.1.1.7.1.1.1.1.1.4.1.1.1.2.1.1.2.1.2l.1.1.3.3.1.1.1.1.3.1.2.1.1.w.1.1.1.1.1.d.1.1.1.1.w.1.1.9.2.1.6.1.4.1.18.1.1.1.1.1.1.7.1.2.1.1.3.1.1.1.1.1.1.1.1.1.1.2k.1.e.1.3.1.n.1.7.1.1.1.1.1.1.1.e.1.1.1.1.v.1.1.1.1.7.1.2.c.1.18.1.1.2.1.8.1.1.1.1.3.1.1.2.1.1.1.2.1.2j.1.e.1.5.t.1.1.1.1.2.h.1.1.1.1.1.y.1.1.5.1.1.1.c.1.19.1.1.1.1.8.1.1.1.1.1.1.1.4.1.1.1.1.1.1.1.1.2g.1.f.5.1.r.1.1.1.1.1.l.1.1.1.1.10.1.1.1.2.1.1.e.1.1.1.14.1.1.1.1.1.1.1.7.1.1.1.1.1.1.1.1.1.1.1.1.1.2.1.3.t.1.1.1.1.1.1h.1.1.f.4.1.3.1.1.m.1.1.1.1.o.1.1.1.1.12.2.1.g.1.1.1.12.1.1.1.1.1.1.2.1.5.1.1.1.1.1.1.1.1.2.1.1.1.1.1.1.4.1.o.1.1.1.1.4.1.1.1.1.1.1.1d.1.1.1.2.b.3.1.1.5.j.1.1.1.s.1.1.1.1.12.1.1.1.f.1.2.1.12.1.2.1.1.1.2.1.1.2.1.1.1.1.1.3.1.7.6.1.1.n.1.9.1.3.1.y.1.1.1.1.1.9.1.1.1.1.1.1.a.2.1.b.1.7.1.1.1.1.1.1.1.w.1.1.1.1.11.1.1.1.j.1.11.1.1.1.1.1.1.1.2.1.2.1.1.7.7.1.1.1.3.1.1.1.e.1.6.1.f.g.1.1.g.1.4.1.c.1.1.a.2.c.1.2.1.1.1.2.1.1.14.1.1.1.11.1.g.1.1.1.12.1.1.2.1.1.1.1.1.1.1.1.1.1.6.1.7.1.2.1.1.1.3.1.b.1.1.1.1.1.1.1.1.1.1.1.e.1.1.1.1.7.5.1.1.1.f.6.b.1.1.1.a.1.1.h.1.18.1.1.1.1.1b.1.1.1.1.1.1.1.1.1.15.1.1.1.1.1.1.1.1.1.1.1.5.1.1.1.1.1.4.1.9.1.1.1.7.1.5.1.2.1.1.1.f.1.1.1.1.4.1.1.1.1.1.1.1.3.1.1.1.1.1.5.1.1.2.1.6.b.1.1.1.9.1.1t.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.l.1.1.1.1.1.1.3.1.1.1.1.16.1.1.1.4.1.1.4.1.1.2.1.1.1.1.1.d.1.5.1.1.u.1.1.1.1.6.1.8.1.1.1.1.1.2.1.1.7.1.1.9.1.1.1.1.8.1v.1.1.1.n.1.1.1.1.1.1.e.1.1.1.1.1.4.1.3.1.1.13.1.1.1.8.4.1.1.1.1.1.1.1.1.1.2.1.1.9.1.5.1.w.1.1.m.1.2.a.2.1.4.1.1.1.1.1.7.1.1w.1.1.p.2.2.1.d.1.1.1.1.1.2.1.1.1.1.1.1.1.1.14.1.1.7.1.1.1.1.1.3.1.1.3.1.1.1.1.1.1.7.1.1.6.1.s.1.2.2.1.1.k.1.1.1.1.9.1.1.1.4.2.1.8.1.1x.1.w.1.a.1.1.1.2.1.1.1.1.1.3.1.1.1.16.1.5.1.1.1.1.1.1.1.1.1.1.3.1.5.1.1.1.1.1.1.1.1.1.8.1.1.1.1.o.1.1.1.4.l.1.1.1.c.3.1.2.1.1.5.1.1.1z.x.1.9.1.2.1.1.1.1.1.1.1.1c.1.5.1.1.1.3.1.1.1.1.1.1.1.6.1.1.1.1.1.1.1.1.1.7.1.2.1.4.1.1.i.1.7.k.1.d.1.1.2.1.3.1.1.3.1.1.1.1z.1.y.5.1.1.1.1.1.1.1.1.1.1.1.1.1.1d.1.6.2.3.1.1.1.1.1.1.1.1.5.1.3.1.1.1.1.1.6.1.2.1.4.1.2.1.g.1.1.7.1.1.1.e.1.1.1.d.1.1.2.1.4.1.1.1.1.1.1.21.z.3.1.1.5.1.1.1.1.1.4.17.1.1.1.1.1.5.1.1.1.1.1.2.1.1.1.1.1.1.2.1.1.1.1.1.9.1.1.4.1.2.1.4.4.1.1.1.1.c.1.b.1.d.g.1.3.5.2.1.1.1.22.z.2.1.7.4.1b.1.3.1.1.1.1.1.1.1.1.1.2.1.1.2.2.1.1.1.1.1.1.3.1.a.1.1.1.1.1.1.3.1.3.2.1.5.4.1.1.1.1.1.1.1.1.1.c.1.1.1.1.1.7.1.f.1.1.1.1.1.4.1.2.1.1.23.y.1.1.1.7.1.1f.1.1.7.1.5.1.1.1.1.1.1.2.1.1.1.1.1.1.1.1.3.1.1.1.1.4.1.2.1.1.1.3.1.1.1.1.1.1.1.4.1.2.1.1.1.1.4.1.i.1.1.1.1.1.1.f.1.1.1.7.1.1.1.1.25.16.1.1.1.1f.2.6.1.1.4.1.1.2.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.3.1.1.1.1.1.1.1.1.1.1.1.1.2.2.1.1.2.1.1.4.1.1.1.1.1.2.1.1.1.z.1.3.1.1.1.9.1.27.1.14.1.10.i.2.5.1.2.1.3.1.1.3.1.1.1.1.1.1.1.1.1.1.1.1.1.c.1.1.3.1.1.1.7.1.4.1.1.1.1.1.1.1.10.1.1.1.1.1.1.1.1.9.1.28.13.1.1.10.2.1.f.1.1.5.1.1.1.4.1.1.4.1.1.2.1.1.1.1.1.1.e.1.1.1.1.1.3.8.1.2.1.1.1.1.1.1.10.1.1.1.1.1.1.1.1.1.a.1.28.1.11.1.1.13.1.1.1.d.1.1.5.1.9.1.1.1.1.3.1.1.1.1.1.1.e.3.1.4.1.1.1.1.5.1.1.1.2.1.2.1.z.1.2.3.1.1.1.1.7.1.1.1.29.12.1.15.1.d.1.1.4.1.6.1.1.1.1.1.1.1.5.1.1.1.1.1.1.a.1.1.1.1.3.1.1.2.1.3.1.4.1.1.1.1.1.1.1.1.1.10.1.1.1.3.1.2.1.5.1.1.2.29.1.11.1.16.f.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.1.6.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.2.5.1.1.5.1.3.3.3.1.1.1.1.10.1.1.3.1.2.3.2.1.1.2.1.2a.11.1.1l.1.1.1.1.1.1.6.1.1.1.2.1.7.1.1.1.1.3.1.1.1.1.4.3.1.c.1.1.1.6.4.1.1.1.x.5.1.1.1.2.1.1.1.1.2.1.1.2a.1.z.1.1m.3.1.8.2.1.a.1.1.5.1.1.1.1.3.1.3.1.c.8.1.2.1.1.1.1.1.w.1.5.1.1.1.1.1.1.1.1.1.1.1.1.2b.1.1.1.w.1.1l.1.1.4.8.2.1.k.1.1.1.1.4.1.c.8.1.2.4.1.x.1.4.1.2.1.1.1.1.1.1.2.2d.1.1.1.1.1.r.1.1.c.13.1.6.6.1.7.3.1.1.1.4.1.1.1.9.1.1.1.1.6.1.b.7.1.3.1.3.1.y.4.2.1.1.1.1.6.2e.1.1.1.1.1.3.1.l.1.d.19.1.4.1.1.1.8.1.1.3.1.3.3.1.1.1.3.1.2.1.1.1.1.1.1.5.b.1.5.1.4.1.3.y.1.1.6.1.1.2m.1.1.1.1.1.1.1.1.1.e.1.1.4.1n.5.1.a.1.5.1.1.1.4.2.1.1.1.1.1.1.1.1.1.3.1.4.1.b.4.1.5.1.3.1.1.x.1.7.1.7.2f.1.1.1.6.1.1.1.6.1.1.1.1.1.1.1.1.1.2.1n.1.2.1.1.b.1.7.1.4.6.1.1.1.1.1.4.1.2.1.1.1.9.1.2.1.1.5.1.5.1.1.1.u.1.1.6.1.2l.3.1.1.1.6.1.1.1.3.1.1.2.1.1.1.4.1.1.1.1g.3.2.1.1.1.1.1.d.c.6.1.1.1.1.6.1.1.1.1.1.1.8.1.1.1.7.6.1.2.1.7.1.1.1.k.1.1f.1g.1.1.1.1.8.1.1.1.1.b.1.1.1f.1.1.1.1.1.1.2.g.c.7.1.1.a.1.2.1.8.1.6.1.1.6.1.1.2.1.1.1.1.1.1.1.3.1.j.1.6.1.a.2e.1.1.1.1.1.8.1.1.c.2.1.1.1e.3.1.1.1.1.1.f.c.7.1.1.b.1.2.1.1.1.1.5.5.1.a.1.1.1.2.1.1.1.3.1.1.1.h.1.6.1.b.11.1e.1.1.1.1.8.1.1.c.1.1.1.1.1.1f.1.2.1.1.1.1.d.1.c.8.1.1.b.1.2.1.1.1.4.1.5.1.c.1.1.1.1.4.1.2.1.h.1.5.1.2.1.9.13.1d.1.1.1.1.8.1.d.1.1.1.1.1f.3.4.1.1.c.c.8.1.1.b.1.1.1.1.1.1.1.1.1.1.1.1.1.1.3.f.1.1.1.2.3.h.1.1.1.1.2.4.1.8.2h.1.1.2.1.7.d.1.2.1.1.1.1.1c.1.3.4.2.1.b.1.b.9.1.1.b.1.1.1.1.1.8.1.1.1.1.e.2.1.1.1.3.1.f.1.1.1.1.1.1.1.4.9.a.10.2.16.1.1.2.7.d.1.1.2.1.1.1.1c.3.1.4.3.1.b.1.1.1.1.1.6.9.2.1.a.1.2.1.1.1.1.7.1.1.f.1.1.1.1.1.3.1.5.7.2.1.2.1.1.1u.15.3.1.6.b.1.1.1.1.1.1.2.1.1.1b.3.5.4.1.8.1.1.2.1.2.1.5.6.2.1.1.2.b.1.1.1.3.8.1.2.d.1.1.1.1.5.2.1.1.1.1.5.1.1.1.3.1.1u.1.1.1.16.1.6.1.6.1.1.2.1.1.1.1.1.1.1.2.1.1.19.1.1.1.1.5.5.1.6.1.5.3.1.1.3.1.1.1.1.1.1.1.1.2.2.e.2.1.8.1.1.1.c.1.1.1.1.1.5.1.1.1.3.1.1.1.1.1.1.6.1x.1.16.7.5.1.2.7.1.1.1.1.1.1a.1.8.6.1.4.1.6.1.4.1.2.a.2.1.c.3.a.1.1.b.1.4.1.5.1.1.1.1.2.1.1.1.5.5.j.1e.1.10.5.7.1.4.3.5.1.2.1.1.1.1.1.1.1.18.9.6.1.2.1.8.5.1.1.a.3.1.a.1.2.1.c.a.1.6.1.3.1.1.1.2.1.1.1.1.1.1.8.1.j.1e.1.10.5.1.7.1.1.1.1.2.1.5.2.1.1.1.1.1.1.1.1.1.1.1.1.14.9.7.1.1.8.1.6.b.1.3.6.1.1.1.1.3.1.c.9.1.7.1.2.1.2.1.1.1.1.1.2.1.9.1.j.1.h.1u.9.1.1.6.1.2.1.1.1.7.1.1.1.1.1.1.1.1.1.1.1.1.1.1.12.9.8.9.7.c.1.2.1.1.3.1.3.2.1.1.d.8.1.9.2.1.2.1.1.1.1.1.2.1.8.1.1.k.12.1k.1.1.7.1.1.1.3.h.1.u.1.1.1.5.1.1.7.8.9.7.b.1.1.2.1.1.1.1.1.4.1.1.1.e.8.a.1.1.1.2.1.2.1.1.1.4.6.1.1.k.2o.1.1.1.1.1.1.1.1.1.1.1.1.1.i.1.u.1.1.5.1.1.1.1.2.3.8.8.1.7.b.2.1.2.8.h.1.5.1.1.a.1.1.1.1.4.1.1.1.1.9.1.1.j.1.2t.1.2.1.3.1.i.1.u.1.5.1.3.1.1.1.1.1.1.3.1.1.1.1.1.8.7.1.b.1.1.1.1.1.6.1.1.i.5.b.4.6.2.1.9.1.1.j.1.2u.1.1.1.1.1.1.1.1.i.10.1.1.2.1.7.1.2.4.7.7.c.1.1.1.1.1.4.1.1.k.5.b.4.1.3.1.1.1.1.1.9.1.1.1.1.h.k.2d.1.1.1.2.2.h.1.10.1.1.1.1.6.1.3.1.2.1.1.1.1.1.1.1.1.1.6.c.4.1.1.1.1.1.m.1.4.a.1.1.3.1.1.1.1.4.1.9.1.1.1.1.h.2z.1.2.1.1.8.1.1.1.5.1.11.1.1.1.1.1.1.3.1.5.1.1.4.1.3.1.6.8.1.2.1.4.1.1.1.5.1.1.1.h.4.a.1.4.1.1.1.1.3.1.1.9.1.1.1.1.1.11.2f.1.1.7.1.1.1.1.1.1.1.1.1.1.2.11.1.1.3.1.1.2.2.1.1.1.1.1.1.8.1.6.1.7.1.2.5.1.1.3.1.1.1.h.1.2.1.3.a.1.3.1.1.3.1.1.1.1.1.9.2.1.1.1.u.3.1.1.1.1.1w.j.1.1.7.1.1.1.3.1.1.1.1.1.1.12.1.4.1.1.1.2.1.1.1.2.1.8.1.5.1.1.1.1.2.1.1.1.1.1.1.5.1.1.1.1.1.3.h.1.1.2.2.1.a.5.1.3.1.1.1.1.1.8.1.1.1.1.1.1.x.3.1.2f.1.1.1.1.1.1.1.1.1.2.1.1.4.3.1.13.1.1.1.2.2.1.1.3.1.1.8.1.1.1.3.1.2.1.1.1.1.1.1.1.2.7.1.6.i.1.2.2.1.1.a.4.1.1.2.1.1.1.9.1.1.2.1.1.1.a.j.4.1.1.1.1.2h.1.1.1.1.1.1.3.1.9.1.12.1.2.1.1.4.3.1.1.8.1.1.3.1.3.1.7.1.8.1.1.3.1.i.3.1.1.1.1.a.4.1.1.3.1.1.9.1.4.1.1.1.j.e.1.2.1.2h.1.1.1.1.1.3.1.9.1.1.12.1.1.1.1.4.3.1.1.7.1.2.1.1.1.5.1.6.1.a.1.2.n.2.1.a.4.1.1.e.4.1.1.1.1.6.8.2.2.2.1.2.b.1.2.2i.1.2.5.1.1.7.2.1.11.1.1.1.1.4.3.1.1.1.4.1.1.3.9.7.1.9.1.1.1.j.6.1.1.9.1.4.1.1.c.1.1.2.1.1.1.1.1.6.d.7.9.2.n.1q.8.7.1.1.4.1.2.1.1.1.1.10.1.1.4.2.1.1.1.1.1.3.4.1.9.1.7.8.1.2.k.1.5.1.b.1.3.1.1.1.a.1.1.1.2.1.2.1.1.p.3.6.1.2.o.1x.8.6.1.1.1.2.1.1.10.1.1.1.1.1.1.1.5.1.1.1.5.3.1.1.1.1.1.1.1.1.1.4.1.6.1.1.2.1.k.1.i.1.1.2.2.4.5.1.1.2.1.5.1.4.i.8.5.r.1t.3.1.7.3.1.1.1.1.1.1.2.1.1.1.10.1.a.1.1.1.1.4.2.1.2.1.5.1.1.1.1.1.1.1.1.1.1.1.1.4.l.1.i.1.1.1.1.2.4.1.4.1.1.1.1.6.1.3m.8.1.2.4.2.2.1.1.1.1d.1.1.4.1.1.1.a.1.2.4.2.4.1.l.1.i.1.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.1.7.4.z.q.1t.1.7.1.1.2.1.3.2.1.1.1.1.1.1.1e.1.1.1.1.1.1.1.a.1.2.1.5.3.1.m.1.i.1.1.1.1.1.1.1.4.1.1.3.2.1.5.1.1.1.11.a.g.1s.1.1.6.1.1.1.1.1.4.1.1.6.1c.1.1.1.1.1.1.1.1.1.9.1.3.1.5.2.1.n.k.1.1.2.1.1.1.2.1.1.1.1.1.1.2.1.1.1.1.1.1.1.1.1.11.a.1y.1.9.2.1.1.4.1.2.1.c.1.1.1a.2.1.3.1.2.a.4.6.1.1.o.1.k.1.3.1.1.3.7.1.1.1.1.1.1.2.1.1.1.10.n.1k.1.1.7.1.4.1.3.1.f.1.1.1.1.1a.4.1.2.a.1.1.1.1.6.1.p.l.1.1.2.1.1.3.1.6.2.1.1.1.3.1.1.1.1.1.1.1.1.p.2.4.28.1.7.4.1.1.1.3.g.1.1.1.1.1.15.3.3.1.1.1.1.a.1.3.1.4.1.1b.1.1.3.1.1.2.1.5.1.2.1.1.1.1.1.1.1.1.1.1.2.1.1.1.1.3.2.1.3.l.1.b.23.1.2.1.3.1.1.1.g.1.3.1.1.1.1.15.1.1.1.1.2.a.1.1.4.1.1.2.1.1c.1.1.2.1.1.1.2.1.4.2.1.1.1.1.1.1.2.1.1.1.1.1.1.1.1.1.1.1.4.1.2.1.1.k.b.23.1.1.1.6.1.k.1.2.1.1.1.14.1.4.b.1.6.2.d.11.1.3.1.1.2.1.1.1.1.1.2.1.1.1.1.2.1.1.1.1.1.1.1.1.5.1.1.1.5.1.1.1.s.1.e.1p.1.1.6.1.r.1.4.10.1.1.1.1.b.1.6.1.1.c.1.11.1.1.2.1.7.1.3.1.1.1.3.2.1.1.1.1.1.1.4.2.1.1.3.1.1.1.1.4.12.1q.2.5.1.t.1.13.1.1.1.c.8.1.b.2.d.p.1.1.1.9.3.1.1.1.7.1.1.2.1.3.3.1.1.1.1.1.1.1.1.4.e.2e.1.6.v.14.1.1.1.b.8.r.r.1.1.1.1.1.1.1.6.1.1.1.1.7.1.1.2.1.2.4.1.1.1.1.3.1.h.1.2e.5.1.v.1.13.3.1.2.1.1.6.1.7.b.3.d.1.r.1.1.1.1.1.1.1.1.5.5.1.2.1.2.3.1.1.1.2.1.2.6.1.1.1.g.1.z.1e.1.4.1.v.1.h.m.1.3.1.1.2.6.1.7.1.1k.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.5.1.1.1.1.1.1.1.1.1.2.4.1.1.1.1.2.d.8.1.b.f.1.1e.5.v.14.1.7.5.1.1.1.1.5.6.4.1f.1.1.1.1.1.1.1.1.1.1.1.1.b.1.1.1.2.1.1.1.1.1.1.3.1.1.1.1.e.1.h.9.8.1.1e.1.4.1.1.3.1.o.1.14.1.7.5.4.1.4.1.5.1.3.1k.1.1.2.1.1.e.4.1.1.1.7.1.1.4.b.d.4.i.1f.6.1.1.1.1.o.16.7.1.1.1.2.4.1.4.1.d.1.14.d.1.1.6.1.1.3.5.5.1.1.1.1.6.1.3.1.o.b.1q.1.6.3.1.m.1.16.7.1.1.1.1.1.1.3.1.1.1.1.1.1.2.1.4.14.r.1.1.1.1.1.1.4.1.9.1.4.1.5.2q.6.4.1.1.j.1.16.1.7.1.3.1.1.3.1.5.3.1.3.1.13.m.6.1.1.1.1.1.4.1.1.j.9.6.3.1.27.6.6.1.i.17.8.5.1.3.1.1.4.6.1.1.1r.1.2.1.4.1.4.2.j.1.d.5.1.1.1.g.1.1.1.3.1i.1.5.7.i.17.8.7.1.1.1.1.4.6.2.1q.1.1.1.1.5.1.4.2.1.i.1.f.h.5.1.1.1.3.1.1i.1.4.7.i.16.1.8.6.1.3.1.4.4.1.1.2.1q.1.2.1.5.1.4.3.i.1.b.m.1.1.2.2.1.1.1.2.1j.1.3.8.1.g.s.e.9.5.1.1.1.1.1.1.3.1.3.1.1.3.8.1g.1.1.a.1.1.1.1.3.1.3.e.1.8.1.1.1.1.l.2.1.4.1.1.1.1.2.1i.1.2.9.1.f.16.1.1.1.1.1.1.3.1.1.2.1.4.2.1.1.1.3.1.5.8.1g.1.d.1.1.4.i.1.7.1.1.2.4.9.b.1.5.1.1.3.1i.1.1.a.e.1.16.1.5.1.1.1.1.1.1.1.5.3.6.4.1.1o.f.5.i.1.7.1.1.1.1.3.1.3.9.1.e.1.1.1l.1.1.6.1.1.1.e.1.17.7.4.6.2.1.6.4.1o.1.k.1.d.5.1.6.1.1.2.3.1.d.1.a.1.3.1m.1.1.5.1.2.f.9.1.y.1.6.4.1.5.1.1.7.4.7.1.4.19.1.1.1.m.1.c.1.2.2.1.a.3.d.h.2.1j.1.1.5.3.e.1.9.10.6.5.1.3.1.1.1.3.4.4.5.1.1.1c.1.1.q.1.c.1.1.1.f.e.f.1.2.1.1i.2.1.1.1.1.1.3.1.1.c.1a.5.1.6.1.1.1.3.3.1.2.1.3.1.1i.1.s.1.1.c.1.1.3.v.1.d.1i.2.1.2.2.5.9.1.1.1.1a.1.4.6.1.1.1.1.3.4.2.1.3.1j.1.t.1.d.1.1.2.x.2.1s.2.1.4.1.1.3.1.7.1.1.1c.1.4.6.4.3.7.3.1j.v.1.1t.2.3.19.2.7.1.3.6.1.1f.4.5.1.4.2.1.7.2.1.1i.1.w.1.36.2.8.1.2.1.4.1.1g.4.1.1.1.1.1.4.1.1.9.1.1.1j.1.w.1.2d.l.8.2.8.2.1.1.4.1h.4.1.1.7.1.1.1u.1.x.1h.t.v.1.1.1.8.1.1.1.1.4.1h.1.3.9.1.1.1v.x.35.2.b.1.5.1i.1.1.1.7.1.2.1.1v.1.w.p.2g.1.1.a.1.5.1.1i.1.1.1.6.1.1.1.1.1x.w.c.2t.1.1.9.1.1.4.1.1k.8.1.1.1.1y.w.o.2h.1.a.2.1.2.1.1.1k.1.8.1.1.1y.1.b.1.1.h.1.5.30.1.a.3.1.1.1.1m.8.1.20.8.1.1.1.1.1.1.1.3.c.36.1.1.9.4.1.1m.1.7.1.20.1.6.1.1.7.1.2.b.1.30.6.1.1.9.4.1o.2.1.1.1.1.1.21.4.1.1.1.9.1.1.1.b.h.2p.1.1.a.1.1.1.1.1o.1.1.1.25.1.1.1.1.d.1.1.1.9.1.i.1.2n.2.b.4h.1.1.1.8.j.1.1.2l.1.1.1.b.11.3i.1.8.k.1.1.2k.1.1.1.b.11.23.1g.1.2.1.2.1.1.l.1.1.1.2i.1.1.1.7.1.1.1.1.34.1h.1.1.1.1.1.m.1.2.1.2i.1.1.8.4r.2.1.1.l.1.1.1.2j.1.1.5.1.2.17.3k.1.2.1.j.1.1.1.1.2k.1.1.5.1.1.1.4s.1.1.1.j.1.1.1.1.2k.1.1.5.1.1.4t.2.1.j.2.2l.1.1.1.5.1.1.4t.1.1.1.h.1.1.1.1.9.2c.1.1.1.5.4w.i.1.2.1.2m.1.1.1.4.1.5d.1.2.1.2n.1.1.1.3.1.2p.1.1.2n.1.2.2o.1.2.3.1.1.2e.b.1.2n.1.1.1.2o.1.1.1.4.1.5d.a.2h.1.1.1.3.1.34.1.1.4z.1.1.3.1.36.1.2i.2h.1.1.3.1.5e.1.2r.1.1.1.2.7.1.1.1.7x.1.1.1.1.1.1.6.1.1.59.2q.1.1.1.1.1.39.1.4x.1.1.1.1.1.1.1.1.1.l.1.1.w.3l.1.30.1.1.1.1.1.1.1.m.1.4h.34.1.x.8c.8c.1'; + +/** Decoded grid cells as [gx, gy] pairs. */ +export function worldDots(): { x: number; y: number }[] { + const out: { x: number; y: number }[] = []; + let cursor = 0; + for (const part of PACKED.split('.')) { + cursor += parseInt(part, 36); + out.push({ x: cursor % WORLD_GRID_W, y: Math.floor(cursor / WORLD_GRID_W) }); + } + return out; +} From 45ce365920bc61e0dc6d8690a86f9cbe70d9ddf4 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 25 Jul 2026 14:28:09 +0200 Subject: [PATCH 2/5] feat(login): a living world map behind the sign-in panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the drifting plane silhouettes with the thing TREK actually does: routes lighting up between cities across a dot map of the world. Arcs leave a city, a light travels the curve, the destination lands, and the arc retires to make room for the next. The map is drawn once into an offscreen layer and blitted per frame, so only the handful of live arcs cost anything — no shadowBlur in the frame path. The same component covers the sign-in moment via `variant="takeoff"`, where the network fires as one cascade and stays lit instead of cycling. Reduced motion gets the map as a still frame. --- client/src/pages/login/LoginWorld.tsx | 287 ++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) create mode 100644 client/src/pages/login/LoginWorld.tsx diff --git a/client/src/pages/login/LoginWorld.tsx b/client/src/pages/login/LoginWorld.tsx new file mode 100644 index 000000000..8a684f1a3 --- /dev/null +++ b/client/src/pages/login/LoginWorld.tsx @@ -0,0 +1,287 @@ +import React from 'react'; +import { WORLD_GRID_H, WORLD_GRID_W, WORLD_LAT_BOT, WORLD_LAT_TOP, worldDots } from './worldDots'; + +/** + * The login backdrop: TREK's own coastlines as a dot map, with routes lighting up + * between cities across it. + * + * The dots come from the Atlas admin-0 bundle, baked into worldDots.ts — the login + * screen is unauthenticated, so it cannot fetch Atlas at runtime. + * + * Perf: the map itself is drawn once into an offscreen canvas and blitted each + * frame; only the handful of live arcs and their city glows are drawn per frame. + * No shadowBlur in the frame path. + */ + +/** Landmark cities, the anchors every route runs between. */ +const CITIES: [number, number][] = [ + [52.52, 13.4], [48.85, 2.35], [51.5, -0.12], [40.4, -3.7], [41.9, 12.5], [59.33, 18.07], + [41.0, 28.98], [55.75, 37.62], [52.37, 4.9], [64.15, -21.9], [38.72, -9.14], [50.08, 14.44], + [30.04, 31.24], [6.52, 3.38], [-1.29, 36.82], [-33.92, 18.42], [33.57, -7.59], + [25.2, 55.27], [19.08, 72.88], [28.61, 77.21], [13.76, 100.5], [1.35, 103.82], + [-6.21, 106.85], [35.68, 139.69], [37.57, 126.98], [39.9, 116.4], [31.23, 121.47], + [22.32, 114.17], [-33.87, 151.21], [-36.85, 174.76], [-27.47, 153.03], + [49.28, -123.12], [37.77, -122.42], [40.71, -74.01], [41.88, -87.63], [43.65, -79.38], + [19.43, -99.13], [25.76, -80.19], [4.71, -74.07], [-12.05, -77.04], [-23.55, -46.63], + [-34.6, -58.38], [-33.45, -70.67], +]; + +interface Arc { + from: number; + to: number; + /** 0 → 1 progress of the light travelling the arc. */ + t: number; + speed: number; + /** Stays lit briefly after arrival, then fades. */ + hold: number; + lift: number; + /** Milliseconds still to wait before this one starts (takeoff cascade). */ + delay: number; +} + +const MAX_ARCS = 7; +const DOT_COLOUR = 'rgba(148, 163, 214, 0.55)'; + +interface LoginWorldProps { + /** + * `ambient` is the login backdrop: a few routes at a time, forever. + * `takeoff` is the sign-in moment: the whole network fires at once and stays lit. + */ + variant?: 'ambient' | 'takeoff'; +} + +export default function LoginWorld({ variant = 'ambient' }: LoginWorldProps): React.ReactElement { + const canvasRef = React.useRef(null); + const takeoff = variant === 'takeoff'; + + React.useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false; + const dots = worldDots(); + + let raf = 0; + let width = 0; + let height = 0; + let dpr = 1; + let mapLayer: HTMLCanvasElement | null = null; + /** Map placement in CSS pixels, shared by the layer and the city projection. */ + let originX = 0; + let originY = 0; + let cell = 1; + + const project = (lat: number, lon: number) => ({ + x: originX + ((lon + 180) / 360) * (WORLD_GRID_W - 1) * cell, + y: originY + ((WORLD_LAT_TOP - lat) / (WORLD_LAT_TOP - WORLD_LAT_BOT)) * (WORLD_GRID_H - 1) * cell, + }); + + /** Redraw the static dot map. Called on mount and on resize only. */ + function buildMap() { + // Cover the panel, biased up a little so the busier northern half sits behind + // the copy rather than under it. + cell = Math.max(width / (WORLD_GRID_W - 1), height / (WORLD_GRID_H - 1)) * 1.08; + originX = (width - (WORLD_GRID_W - 1) * cell) / 2; + originY = (height - (WORLD_GRID_H - 1) * cell) / 2; + + const layer = document.createElement('canvas'); + layer.width = Math.max(1, Math.floor(width * dpr)); + layer.height = Math.max(1, Math.floor(height * dpr)); + const lc = layer.getContext('2d'); + if (!lc) return; + lc.scale(dpr, dpr); + lc.fillStyle = DOT_COLOUR; + const r = Math.max(0.6, cell * 0.19); + for (const d of dots) { + const x = originX + d.x * cell; + const y = originY + d.y * cell; + if (x < -4 || x > width + 4 || y < -4 || y > height + 4) continue; + lc.beginPath(); + lc.arc(x, y, r, 0, Math.PI * 2); + lc.fill(); + } + mapLayer = layer; + } + + function resize() { + const rect = canvas!.getBoundingClientRect(); + dpr = Math.min(window.devicePixelRatio || 1, 2); + width = rect.width; + height = rect.height; + canvas!.width = Math.max(1, Math.floor(width * dpr)); + canvas!.height = Math.max(1, Math.floor(height * dpr)); + ctx!.setTransform(dpr, 0, 0, dpr, 0, 0); + buildMap(); + } + + const arcs: Arc[] = []; + + function spawnArc(opts?: { delay?: number; speed?: number }) { + if (!takeoff && arcs.length >= MAX_ARCS) return; + const from = Math.floor(Math.random() * CITIES.length); + let to = Math.floor(Math.random() * CITIES.length); + if (to === from) to = (to + 1 + Math.floor(Math.random() * 3)) % CITIES.length; + arcs.push({ + from, + to, + t: 0, + // Per millisecond, so 1/speed is the flight time: roughly 3–5s. Long enough + // to read as a journey, short enough that the map keeps moving. + speed: opts?.speed ?? 0.0002 + Math.random() * 0.00013, + hold: 0, + // Longer hops bow higher, the way a route reads on a map. + lift: 0.16 + Math.random() * 0.16, + delay: opts?.delay ?? 0, + }); + } + + /** + * Sign-in: the whole network lights up at once. Routes leave in a staggered + * cascade so it reads as departures rather than one flash, and nothing fades — + * the finished web is the last thing on screen before the app takes over. + */ + function burst() { + for (let i = 0; i < 11; i++) { + spawnArc({ delay: i * 105 + Math.random() * 70, speed: 0.0013 + Math.random() * 0.0009 }); + } + } + + /** Quadratic arc between two projected cities, bowed away from the straight line. */ + function arcPoints(a: Arc) { + const p1 = project(CITIES[a.from][0], CITIES[a.from][1]); + const p2 = project(CITIES[a.to][0], CITIES[a.to][1]); + const mx = (p1.x + p2.x) / 2; + const my = (p1.y + p2.y) / 2; + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + const len = Math.hypot(dx, dy) || 1; + // Perpendicular offset, always bowing "up" so arcs never dive off-screen. + const cx = mx - (dy / len) * len * a.lift * Math.sign(dx || 1); + const cy = my + (dx / len) * len * a.lift * Math.sign(dx || 1) - len * 0.08; + return { p1, p2, cx, cy }; + } + + function pointOn(p1: { x: number; y: number }, cx: number, cy: number, p2: { x: number; y: number }, t: number) { + const u = 1 - t; + return { + x: u * u * p1.x + 2 * u * t * cx + t * t * p2.x, + y: u * u * p1.y + 2 * u * t * cy + t * t * p2.y, + }; + } + + function cityGlow(x: number, y: number, alpha: number, radius: number) { + const g = ctx!.createRadialGradient(x, y, 0, x, y, radius); + g.addColorStop(0, `rgba(199, 210, 254, ${alpha})`); + g.addColorStop(0.35, `rgba(129, 140, 248, ${alpha * 0.5})`); + g.addColorStop(1, 'rgba(99, 102, 241, 0)'); + ctx!.fillStyle = g; + ctx!.beginPath(); + ctx!.arc(x, y, radius, 0, Math.PI * 2); + ctx!.fill(); + } + + let last = 0; + let spawnTimer = 0; + + function frame(now: number) { + const dt = last ? Math.min(now - last, 48) : 16; + last = now; + + ctx!.clearRect(0, 0, width, height); + if (mapLayer) ctx!.drawImage(mapLayer, 0, 0, width, height); + + if (!takeoff) { + spawnTimer -= dt; + if (spawnTimer <= 0) { + spawnArc(); + spawnTimer = 1600 + Math.random() * 2200; + } + } + + for (let i = arcs.length - 1; i >= 0; i--) { + const a = arcs[i]; + + if (a.delay > 0) { + a.delay -= dt; + continue; + } + + const { p1, p2, cx, cy } = arcPoints(a); + + if (a.t < 1) a.t = Math.min(1, a.t + a.speed * dt); + else a.hold += dt; + + // Ambient routes retire to make room for the next; the takeoff web stays lit. + const fade = takeoff ? 1 : a.hold > 2200 ? Math.max(0, 1 - (a.hold - 2200) / 2400) : 1; + if (fade <= 0) { + arcs.splice(i, 1); + continue; + } + + // The drawn portion, with the leading edge brightest. + ctx!.strokeStyle = `rgba(129, 140, 248, ${0.5 * fade})`; + ctx!.lineWidth = 1.4; + ctx!.beginPath(); + const segs = Math.max(2, Math.round(34 * a.t)); + for (let s = 0; s <= segs; s++) { + const pt = pointOn(p1, cx, cy, p2, (a.t * s) / segs); + if (s === 0) ctx!.moveTo(pt.x, pt.y); + else ctx!.lineTo(pt.x, pt.y); + } + ctx!.stroke(); + + // Origin lights the moment the route leaves it, destination on arrival. + cityGlow(p1.x, p1.y, 0.55 * fade, Math.max(9, cell * 2.6)); + if (a.t >= 1) cityGlow(p2.x, p2.y, 0.6 * fade, Math.max(11, cell * 3)); + + // The travelling head. + if (a.t < 1) { + const head = pointOn(p1, cx, cy, p2, a.t); + cityGlow(head.x, head.y, 0.75, Math.max(7, cell * 2)); + ctx!.fillStyle = 'rgba(224, 242, 254, 0.95)'; + ctx!.beginPath(); + ctx!.arc(head.x, head.y, 1.8, 0, Math.PI * 2); + ctx!.fill(); + } + } + + raf = window.requestAnimationFrame(frame); + } + + resize(); + window.addEventListener('resize', resize); + + if (reduced) { + // Still frame: the map, no routes firing. + ctx.clearRect(0, 0, width, height); + if (mapLayer) ctx.drawImage(mapLayer, 0, 0, width, height); + } else if (takeoff) { + burst(); + raf = window.requestAnimationFrame(frame); + } else { + // Open with a few routes already in flight instead of an empty map. + for (let i = 0; i < 3; i++) { + spawnArc(); + const a = arcs[arcs.length - 1]; + if (a) a.t = Math.random() * 0.6; + } + raf = window.requestAnimationFrame(frame); + } + + return () => { + window.cancelAnimationFrame(raf); + window.removeEventListener('resize', resize); + }; + }, [takeoff]); + + return ( + + ); +} From 8b6a80fce04734860e72662d919d4ece45e32172 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 25 Jul 2026 14:28:20 +0200 Subject: [PATCH 3/5] feat(login): rebuild the branding panel and the sign-in moment around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The panel dropped the eight feature tiles and the self-hosted line — they read as a spec sheet on a screen whose job is to get you in. What is left is the mark, the tagline and one line of copy, centred over the map, each with a soft dark halo so the dots cannot eat the type. The tagline and subtitle never wrap: they scale with the panel instead, since it is a fixed share of the viewport. The takeoff after sign-in used to tell a different story than the screen it came from — cloud ellipses, a lone plane, a contrail. It now picks the panel up and escalates it: the same map, every route departing at once, the logo landing as the network completes, then a soft hand-off into the app. --- client/src/pages/LoginPage.tsx | 604 +++++++++++---------------------- 1 file changed, 192 insertions(+), 412 deletions(-) diff --git a/client/src/pages/LoginPage.tsx b/client/src/pages/LoginPage.tsx index 93308e803..ebb0abf69 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/pages/LoginPage.tsx @@ -1,28 +1,41 @@ import { - BookMarked, - CheckSquare, ChevronDown, Eye, EyeOff, Fingerprint, - FolderOpen, Globe, KeyRound, Lock, Mail, - Map, Plane, - Route, Shield, User, - Users, - Wallet, - Zap, } from 'lucide-react'; import React from 'react'; import ToggleSwitch from '../components/Settings/ToggleSwitch'; import { SUPPORTED_LANGUAGES, useTranslation } from '../i18n'; import { useLogin } from './login/useLogin'; +import LoginWorld from './login/LoginWorld'; + +/** Fixed so the sky does not reshuffle on every render. */ +const STARFIELD = [ + { top: 6, left: 12, size: 1, opacity: 0.3, delay: 0 }, + { top: 11, left: 68, size: 2, opacity: 0.22, delay: 1.4 }, + { top: 17, left: 34, size: 1, opacity: 0.35, delay: 2.6 }, + { top: 22, left: 84, size: 1, opacity: 0.25, delay: 0.7 }, + { top: 28, left: 19, size: 2, opacity: 0.2, delay: 3.1 }, + { top: 33, left: 57, size: 1, opacity: 0.32, delay: 1.9 }, + { top: 39, left: 91, size: 1, opacity: 0.24, delay: 2.2 }, + { top: 46, left: 8, size: 1, opacity: 0.28, delay: 0.4 }, + { top: 52, left: 73, size: 2, opacity: 0.18, delay: 3.6 }, + { top: 58, left: 42, size: 1, opacity: 0.3, delay: 1.1 }, + { top: 64, left: 88, size: 1, opacity: 0.26, delay: 2.9 }, + { top: 71, left: 26, size: 1, opacity: 0.22, delay: 0.9 }, + { top: 77, left: 63, size: 1, opacity: 0.3, delay: 3.3 }, + { top: 83, left: 15, size: 2, opacity: 0.2, delay: 1.7 }, + { top: 88, left: 79, size: 1, opacity: 0.27, delay: 2.4 }, + { top: 93, left: 48, size: 1, opacity: 0.23, delay: 0.2 }, +]; export default function LoginPage(): React.ReactElement { const { t, language } = useTranslation(); @@ -96,190 +109,110 @@ export default function LoginPage(): React.ReactElement { if (showTakeoff) { return ( -
- {/* Sky gradient */} -
- - {/* Stars */} - {Array.from({ length: 60 }, (_, i) => ( -
0.7 ? 3 : 1.5, - height: Math.random() > 0.7 ? 3 : 1.5, - borderRadius: '50%', - background: 'white', - top: `${Math.random() * 100}%`, - left: `${Math.random() * 100}%`, - animationDelay: `${0.3 + Math.random() * 0.5}s, ${Math.random() * 1}s`, - }} - /> - ))} - - {/* Clouds rushing past */} - {[0, 1, 2, 3, 4].map((i) => ( -
- ))} - - {/* Speed lines */} - {Array.from({ length: 12 }, (_, i) => ( -
- ))} - - {/* Plane */} -
- - - - - - - - - - +
+ {/* Signing in picks up exactly where the login panel left off: the same dot + map, except now every route departs at once. The network finishing is + the moment — no separate imagery, no plane flying off alone. */} +
+
- {/* Contrail */} -
+ {/* The colour rises with the departures rather than sitting there from the start. */} +
+
- {/* Logo fade in + burst */} -
- TREK +
+ TREK

{t('login.tagline')}

+ {/* Hands over to the app instead of cutting to it. */} +
+
@@ -389,7 +322,7 @@ export default function LoginPage(): React.ReactElement { style={{ display: 'none', width: '55%', - background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)', + background: '#070c1a', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', @@ -401,240 +334,87 @@ export default function LoginPage(): React.ReactElement { > - {/* Stars */} + {/* Aurora — three drifting colour fields. They are the whole background: + deep indigo, a cold cyan and a warm violet, blurred past recognition so + what is left is the gradient, not the shapes. */} +
+
+
+ + {/* Depth: a handful of pinpricks, far enough back to read as distance. */}
- {Array.from({ length: 40 }, (_, i) => ( + {STARFIELD.map((s, i) => (
0.7 ? 2 : 1, - height: Math.random() > 0.7 ? 2 : 1, + width: s.size, + height: s.size, borderRadius: '50%', background: 'white', - opacity: 0.15 + Math.random() * 0.25, - top: `${Math.random() * 70}%`, - left: `${Math.random() * 100}%`, - animationDelay: `${Math.random() * 4}s`, + opacity: s.opacity, + top: `${s.top}%`, + left: `${s.left}%`, + animationDelay: `${s.delay}s`, }} /> ))}
- {/* Animated glow orbs */} -
-
+ {/* Coastlines as a dot map, with routes lighting up between cities across + it. The geometry is TREK's own Atlas bundle, baked in at build time + because this screen is unauthenticated. */} + - {/* Animated planes — realistic silhouettes at different sizes/speeds */} -
- {/* Plane 1 — large, slow, foreground */} - - - - - - - - - - - - {/* Plane 2 — small, faster, higher */} - - - - - - - - - - {/* Plane 3 — medium, mid-speed */} - - - - - - - - - - - - {/* Plane 4 — tiny, fast, high */} - - - - - - - - - - {/* Plane 5 — medium, right to left, lower */} - - - - - - - - - - - - {/* Plane 6 — tiny distant */} - - - - - - - - -
- -
- {/* Logo */} -
- TREK + {/* No max-width: the tagline stays on one line, so the block is allowed to + use the whole panel rather than wrapping inside it. */} +
+ {/* Logo. The soft dark halo is what separates it from the dot map behind — + wide and low-opacity rather than a hard drop shadow, so it reads as the + map dimming around the mark instead of an outline. */} +
+ TREK

{t('login.tagline')}

{t('login.description')}

- -
- {[ - { Icon: Map, label: t('login.features.maps'), desc: t('login.features.mapsDesc') }, - { Icon: Zap, label: t('login.features.realtime'), desc: t('login.features.realtimeDesc') }, - { Icon: Wallet, label: t('login.features.budget'), desc: t('login.features.budgetDesc') }, - { Icon: Users, label: t('login.features.collab'), desc: t('login.features.collabDesc') }, - { Icon: CheckSquare, label: t('login.features.packing'), desc: t('login.features.packingDesc') }, - { Icon: BookMarked, label: t('login.features.bookings'), desc: t('login.features.bookingsDesc') }, - { Icon: FolderOpen, label: t('login.features.files'), desc: t('login.features.filesDesc') }, - { Icon: Route, label: t('login.features.routes'), desc: t('login.features.routesDesc') }, - ].map(({ Icon, label, desc }) => ( -
) => { - e.currentTarget.style.background = 'rgba(255,255,255,0.08)'; - e.currentTarget.style.borderColor = 'rgba(255,255,255,0.12)'; - }} - onMouseLeave={(e: React.MouseEvent) => { - e.currentTarget.style.background = 'rgba(255,255,255,0.04)'; - e.currentTarget.style.borderColor = 'rgba(255,255,255,0.06)'; - }} - > - -
- {label} -
-
- {desc} -
-
- ))} -
- -

- {t('login.selfHosted')} -

@@ -1441,20 +1221,45 @@ export default function LoginPage(): React.ReactElement {
From bccf4284c42330e6a95e2e0b8e54037b9a58b12b Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 25 Jul 2026 14:28:21 +0200 Subject: [PATCH 4/5] i18n(login): drop the keys the feature tiles used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit login.features.* and login.selfHosted across all 23 locales — nothing renders them any more. --- shared/src/i18n/ar/login.ts | 17 ----------------- shared/src/i18n/br/login.ts | 17 ----------------- shared/src/i18n/ca/login.ts | 17 ----------------- shared/src/i18n/cs/login.ts | 17 ----------------- shared/src/i18n/de/login.ts | 17 ----------------- shared/src/i18n/en/login.ts | 17 ----------------- shared/src/i18n/es/login.ts | 17 ----------------- shared/src/i18n/fr/login.ts | 17 ----------------- shared/src/i18n/gr/login.ts | 17 ----------------- shared/src/i18n/hu/login.ts | 17 ----------------- shared/src/i18n/id/login.ts | 17 ----------------- shared/src/i18n/it/login.ts | 17 ----------------- shared/src/i18n/ja/login.ts | 17 ----------------- shared/src/i18n/ko/login.ts | 17 ----------------- shared/src/i18n/nl/login.ts | 17 ----------------- shared/src/i18n/pl/login.ts | 17 ----------------- shared/src/i18n/ru/login.ts | 17 ----------------- shared/src/i18n/sv/login.ts | 17 ----------------- shared/src/i18n/tr/login.ts | 17 ----------------- shared/src/i18n/uk/login.ts | 17 ----------------- shared/src/i18n/vi/login.ts | 17 ----------------- shared/src/i18n/zh-TW/login.ts | 17 ----------------- shared/src/i18n/zh/login.ts | 17 ----------------- 23 files changed, 391 deletions(-) diff --git a/shared/src/i18n/ar/login.ts b/shared/src/i18n/ar/login.ts index 83808f2e9..38655a091 100644 --- a/shared/src/i18n/ar/login.ts +++ b/shared/src/i18n/ar/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'فشل تسجيل الدخول. يرجى التحقق من بياناتك.', 'login.tagline': 'رحلاتك.\nخطتك.', 'login.description': 'خطط لرحلاتك بشكل تعاوني مع خرائط تفاعلية وميزانيات ومزامنة لحظية.', - 'login.features.maps': 'خرائط تفاعلية', - 'login.features.mapsDesc': 'Google Places ومسارات وتجميع', - 'login.features.realtime': 'مزامنة فورية', - 'login.features.realtimeDesc': 'خططوا معًا عبر WebSocket', - 'login.features.budget': 'تتبع التكاليف', - 'login.features.budgetDesc': 'فئات ورسوم وتقسيم لكل شخص', - 'login.features.collab': 'تعاون', - 'login.features.collabDesc': 'عدة مستخدمين مع رحلات مشتركة', - 'login.features.packing': 'قوائم تجهيز', - 'login.features.packingDesc': 'فئات وتقدم واقتراحات', - 'login.features.bookings': 'الحجوزات', - 'login.features.bookingsDesc': 'رحلات وفنادق ومطاعم وغير ذلك', - 'login.features.files': 'المستندات', - 'login.features.filesDesc': 'رفع الملفات وإدارتها', - 'login.features.routes': 'مسارات ذكية', - 'login.features.routesDesc': 'تحسين تلقائي وتصدير إلى Google Maps', - 'login.selfHosted': 'استضافة ذاتية · مفتوح المصدر · بياناتك تبقى ملكك', 'login.title': 'تسجيل الدخول', 'login.subtitle': 'مرحبًا بعودتك', 'login.signingIn': 'جارٍ تسجيل الدخول…', diff --git a/shared/src/i18n/br/login.ts b/shared/src/i18n/br/login.ts index 2a61c454b..194cac1ed 100644 --- a/shared/src/i18n/br/login.ts +++ b/shared/src/i18n/br/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Falha no login. Verifique suas credenciais.', 'login.tagline': 'Suas viagens.\nSeu plano.', 'login.description': 'Planeje viagens em equipe com mapas interativos, orçamento e sincronização em tempo real.', - 'login.features.maps': 'Mapas interativos', - 'login.features.mapsDesc': 'Google Places, rotas e agrupamento', - 'login.features.realtime': 'Sincronização em tempo real', - 'login.features.realtimeDesc': 'Planejem juntos via WebSocket', - 'login.features.budget': 'Controle de custos', - 'login.features.budgetDesc': 'Categorias, gráficos e custo por pessoa', - 'login.features.collab': 'Colaboração', - 'login.features.collabDesc': 'Vários usuários com viagens compartilhadas', - 'login.features.packing': 'Listas de malas', - 'login.features.packingDesc': 'Categorias, progresso e sugestões', - 'login.features.bookings': 'Reservas', - 'login.features.bookingsDesc': 'Voos, hotéis, restaurantes e mais', - 'login.features.files': 'Documentos', - 'login.features.filesDesc': 'Envie e gerencie documentos', - 'login.features.routes': 'Rotas inteligentes', - 'login.features.routesDesc': 'Otimize e exporte para o Google Maps', - 'login.selfHosted': 'Auto-hospedado · Código aberto · Seus dados são seus', 'login.title': 'Entrar', 'login.subtitle': 'Bem-vindo de volta', 'login.signingIn': 'Entrando…', diff --git a/shared/src/i18n/ca/login.ts b/shared/src/i18n/ca/login.ts index 3aabfa8b4..78bda4729 100644 --- a/shared/src/i18n/ca/login.ts +++ b/shared/src/i18n/ca/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Els teus viatges.\nEl teu pla.', 'login.description': 'Planifica viatges en col·laboració amb mapes interactius, pressupostos i sincronització en temps real.', - 'login.features.maps': 'Mapes interactius', - 'login.features.mapsDesc': 'Google Places, rutes i agrupació', - 'login.features.realtime': 'Sincronització en temps real', - 'login.features.realtimeDesc': 'Planifiqueu junts mitjançant WebSocket', - 'login.features.budget': 'Control de pressupost', - 'login.features.budgetDesc': 'Categories, gràfics i costos per persona', - 'login.features.collab': 'Col·laboració', - 'login.features.collabDesc': 'Multiusuari amb viatges compartits', - 'login.features.packing': "Llistes d'equipatge", - 'login.features.packingDesc': 'Categories, progrés i suggeriments', - 'login.features.bookings': 'Reserves', - 'login.features.bookingsDesc': 'Vols, hotels, restaurants i més', - 'login.features.files': 'Documents', - 'login.features.filesDesc': 'Puja i gestiona documents', - 'login.features.routes': 'Rutes intel·ligents', - 'login.features.routesDesc': 'Optimització automàtica i exportació a Google Maps', - 'login.selfHosted': 'Autoallotjat · Codi obert · Les teves dades segueixen sent teves', 'login.title': 'Inicia la sessió', 'login.subtitle': 'Benvingut de nou', 'login.signingIn': 'Iniciant la sessió…', diff --git a/shared/src/i18n/cs/login.ts b/shared/src/i18n/cs/login.ts index 97518ac52..e7c8a5fa8 100644 --- a/shared/src/i18n/cs/login.ts +++ b/shared/src/i18n/cs/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Přihlášení se nezdařilo. Zkontrolujte prosím své údaje.', 'login.tagline': 'Vaše cesty.\nVáš plán.', 'login.description': 'Plánujte cesty společně s interaktivními mapami, rozpočty a synchronizací v reálném čase.', - 'login.features.maps': 'Interaktivní mapy', - 'login.features.mapsDesc': 'Google Places, trasy a shlukování bodů', - 'login.features.realtime': 'Synchronizace v reálném čase', - 'login.features.realtimeDesc': 'Plánujte společně přes WebSocket', - 'login.features.budget': 'Sledování nákladů', - 'login.features.budgetDesc': 'Kategorie, grafy a náklady na osobu', - 'login.features.collab': 'Spolupráce', - 'login.features.collabDesc': 'Více uživatelů se sdílenými cestami', - 'login.features.packing': 'Seznamy věcí', - 'login.features.packingDesc': 'Kategorie, pokrok v balení a návrhy', - 'login.features.bookings': 'Rezervace', - 'login.features.bookingsDesc': 'Lety, hotely, restaurace a další', - 'login.features.files': 'Dokumenty', - 'login.features.filesDesc': 'Nahrávejte a spravujte dokumenty', - 'login.features.routes': 'Chytré trasy', - 'login.features.routesDesc': 'Automatická optimalizace a export do Google Maps', - 'login.selfHosted': 'Self-hosted · Open Source · Vaše data zůstávají u vás', 'login.title': 'Přihlásit se', 'login.subtitle': 'Vítejte zpět', 'login.signingIn': 'Přihlašování…', diff --git a/shared/src/i18n/de/login.ts b/shared/src/i18n/de/login.ts index 4c43c8551..1ca952f97 100644 --- a/shared/src/i18n/de/login.ts +++ b/shared/src/i18n/de/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.', 'login.tagline': 'Deine Reisen.\nDein Plan.', 'login.description': 'Plane Reisen gemeinsam mit interaktiven Karten, Budgets und Echtzeit-Sync.', - 'login.features.maps': 'Interaktive Karten', - 'login.features.mapsDesc': 'Google Places, Routen & Clustering', - 'login.features.realtime': 'Echtzeit-Sync', - 'login.features.realtimeDesc': 'Gemeinsam planen via WebSocket', - 'login.features.budget': 'Kosten-Tracking', - 'login.features.budgetDesc': 'Kategorien, Diagramme & Pro-Kopf', - 'login.features.collab': 'Zusammenarbeit', - 'login.features.collabDesc': 'Multi-User mit geteilten Reisen', - 'login.features.packing': 'Packlisten', - 'login.features.packingDesc': 'Kategorien & Fortschritt', - 'login.features.bookings': 'Buchungen', - 'login.features.bookingsDesc': 'Flüge, Hotels, Restaurants & mehr', - 'login.features.files': 'Dokumente', - 'login.features.filesDesc': 'Dateien hochladen & verwalten', - 'login.features.routes': 'Routenoptimierung', - 'login.features.routesDesc': 'Auto-Optimierung & Google Maps Export', - 'login.selfHosted': 'Self-hosted · Open Source · Deine Daten bleiben bei dir', 'login.title': 'Anmelden', 'login.subtitle': 'Willkommen zurück', 'login.signingIn': 'Anmelden…', diff --git a/shared/src/i18n/en/login.ts b/shared/src/i18n/en/login.ts index f3631ea40..926eb1eb8 100644 --- a/shared/src/i18n/en/login.ts +++ b/shared/src/i18n/en/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Login failed. Please check your credentials.', 'login.tagline': 'Your Trips.\nYour Plan.', 'login.description': 'Plan trips collaboratively with interactive maps, budgets, and real-time sync.', - 'login.features.maps': 'Interactive Maps', - 'login.features.mapsDesc': 'Google Places, routes & clustering', - 'login.features.realtime': 'Real-Time Sync', - 'login.features.realtimeDesc': 'Plan together via WebSocket', - 'login.features.budget': 'Cost Tracking', - 'login.features.budgetDesc': 'Categories, charts & per-person costs', - 'login.features.collab': 'Collaboration', - 'login.features.collabDesc': 'Multi-user with shared trips', - 'login.features.packing': 'Packing Lists', - 'login.features.packingDesc': 'Categories, progress & suggestions', - 'login.features.bookings': 'Reservations', - 'login.features.bookingsDesc': 'Flights, hotels, restaurants & more', - 'login.features.files': 'Documents', - 'login.features.filesDesc': 'Upload & manage documents', - 'login.features.routes': 'Smart Routes', - 'login.features.routesDesc': 'Auto-optimize & Google Maps export', - 'login.selfHosted': 'Self-hosted · Open Source · Your data stays yours', 'login.title': 'Sign In', 'login.subtitle': 'Welcome back', 'login.signingIn': 'Signing in…', diff --git a/shared/src/i18n/es/login.ts b/shared/src/i18n/es/login.ts index f6992f17d..de96659c6 100644 --- a/shared/src/i18n/es/login.ts +++ b/shared/src/i18n/es/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Tus viajes.\nTu plan.', 'login.description': 'Planifica viajes en colaboración con mapas interactivos, presupuestos y sincronización en tiempo real.', - 'login.features.maps': 'Mapas interactivos', - 'login.features.mapsDesc': 'Google Places, rutas y agrupación', - 'login.features.realtime': 'Sincronización en tiempo real', - 'login.features.realtimeDesc': 'Planificad juntos mediante WebSocket', - 'login.features.budget': 'Control de costes', - 'login.features.budgetDesc': 'Categorías, gráficos y costes por persona', - 'login.features.collab': 'Colaboración', - 'login.features.collabDesc': 'Multiusuario con viajes compartidos', - 'login.features.packing': 'Listas de equipaje', - 'login.features.packingDesc': 'Categorías, progreso y sugerencias', - 'login.features.bookings': 'Reservas', - 'login.features.bookingsDesc': 'Vuelos, hoteles, restaurantes y más', - 'login.features.files': 'Documentos', - 'login.features.filesDesc': 'Sube y gestiona documentos', - 'login.features.routes': 'Rutas inteligentes', - 'login.features.routesDesc': 'Optimización automática y exportación a Google Maps', - 'login.selfHosted': 'Autoalojado · Código abierto · Tus datos siguen siendo tuyos', 'login.title': 'Iniciar sesión', 'login.subtitle': 'Bienvenido de nuevo', 'login.signingIn': 'Iniciando sesión…', diff --git a/shared/src/i18n/fr/login.ts b/shared/src/i18n/fr/login.ts index c8a907089..90f43a243 100644 --- a/shared/src/i18n/fr/login.ts +++ b/shared/src/i18n/fr/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Vos voyages.\nVotre organisation.', 'login.description': 'Planifiez vos voyages en collaboration avec des cartes interactives, des budgets et la synchronisation en temps réel.', - 'login.features.maps': 'Cartes interactives', - 'login.features.mapsDesc': 'Google Places, itinéraires et regroupement', - 'login.features.realtime': 'Synchronisation en temps réel', - 'login.features.realtimeDesc': 'Planifiez ensemble via WebSocket', - 'login.features.budget': 'Suivi des coûts', - 'login.features.budgetDesc': 'Catégories, graphiques et coûts par personne', - 'login.features.collab': 'Collaboration', - 'login.features.collabDesc': 'Multi-utilisateurs avec voyages partagés', - 'login.features.packing': 'Listes de bagages', - 'login.features.packingDesc': 'Catégories, progression et suggestions', - 'login.features.bookings': 'Réservations', - 'login.features.bookingsDesc': 'Vols, hôtels, restaurants et plus', - 'login.features.files': 'Documents', - 'login.features.filesDesc': 'Importez et gérez vos documents', - 'login.features.routes': 'Itinéraires intelligents', - 'login.features.routesDesc': 'Optimisation automatique et export Google Maps', - 'login.selfHosted': 'Auto-hébergé · Open Source · Vos données restent les vôtres', 'login.title': 'Connexion', 'login.subtitle': 'Bon retour', 'login.signingIn': 'Connexion en cours…', diff --git a/shared/src/i18n/gr/login.ts b/shared/src/i18n/gr/login.ts index 0f003c3cd..129f33be1 100644 --- a/shared/src/i18n/gr/login.ts +++ b/shared/src/i18n/gr/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Τα Ταξίδια σας.\nΤο Σχέδιό σας.', 'login.description': 'Σχεδιάστε ταξίδια συνεργατικά με διαδραστικούς χάρτες, προϋπολογισμούς και συγχρονισμό σε πραγματικό χρόνο.', - 'login.features.maps': 'Διαδραστικοί Χάρτες', - 'login.features.mapsDesc': 'Google Places, διαδρομές & ομαδοποίηση', - 'login.features.realtime': 'Συγχρονισμός Πραγματικού Χρόνου', - 'login.features.realtimeDesc': 'Σχεδιάστε μαζί μέσω WebSocket', - 'login.features.budget': 'Παρακολούθηση Κόστους', - 'login.features.budgetDesc': 'Κατηγορίες, γραφήματα & κόστος ανά άτομο', - 'login.features.collab': 'Συνεργασία', - 'login.features.collabDesc': 'Πολλαπλοί χρήστες με κοινόχρηστα ταξίδια', - 'login.features.packing': 'Λίστες Πακεταρίσματος', - 'login.features.packingDesc': 'Κατηγορίες, πρόοδος & προτάσεις', - 'login.features.bookings': 'Κρατήσεις', - 'login.features.bookingsDesc': 'Πτήσεις, ξενοδοχεία, εστιατόρια & άλλα', - 'login.features.files': 'Έγγραφα', - 'login.features.filesDesc': 'Μεταφόρτωση & διαχείριση εγγράφων', - 'login.features.routes': 'Έξυπνες Διαδρομές', - 'login.features.routesDesc': 'Αυτόματη βελτιστοποίηση & εξαγωγή στο Google Maps', - 'login.selfHosted': 'Self-hosted \u00B7 Open Source \u00B7 Τα δεδομένα σας παραμένουν δικά σας', 'login.title': 'Σύνδεση', 'login.subtitle': 'Καλώς ήρθατε πίσω', 'login.signingIn': 'Σύνδεση…', diff --git a/shared/src/i18n/hu/login.ts b/shared/src/i18n/hu/login.ts index 7075050f4..1a69db77f 100644 --- a/shared/src/i18n/hu/login.ts +++ b/shared/src/i18n/hu/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Az utazásaid.\nA terved.', 'login.description': 'Tervezz utazásokat közösen interaktív térképekkel, költségvetéssel és valós idejű szinkronizálással.', - 'login.features.maps': 'Interaktív térképek', - 'login.features.mapsDesc': 'Google Places, útvonalak és csoportosítás', - 'login.features.realtime': 'Valós idejű szinkron', - 'login.features.realtimeDesc': 'Közös tervezés WebSocket-en keresztül', - 'login.features.budget': 'Költségkövetés', - 'login.features.budgetDesc': 'Kategóriák, diagramok és személyenkénti költségek', - 'login.features.collab': 'Együttműködés', - 'login.features.collabDesc': 'Többfelhasználós, megosztott utazásokkal', - 'login.features.packing': 'Csomagolási listák', - 'login.features.packingDesc': 'Kategóriák és haladás', - 'login.features.bookings': 'Foglalások', - 'login.features.bookingsDesc': 'Repülők, szállodák, éttermek és még több', - 'login.features.files': 'Dokumentumok', - 'login.features.filesDesc': 'Fájlok feltöltése és kezelése', - 'login.features.routes': 'Útvonal-optimalizálás', - 'login.features.routesDesc': 'Automatikus optimalizálás és Google Maps export', - 'login.selfHosted': 'Saját üzemeltetés · Nyílt forráskód · Az adataid nálad maradnak', 'login.title': 'Bejelentkezés', 'login.subtitle': 'Üdv újra', 'login.signingIn': 'Bejelentkezés…', diff --git a/shared/src/i18n/id/login.ts b/shared/src/i18n/id/login.ts index 692eda8d4..86ca7ce42 100644 --- a/shared/src/i18n/id/login.ts +++ b/shared/src/i18n/id/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Login gagal. Periksa kembali kredensial kamu.', 'login.tagline': 'Perjalananmu.\nRencanamu.', 'login.description': 'Rencanakan perjalanan bersama dengan peta interaktif, anggaran, dan sinkronisasi real-time.', - 'login.features.maps': 'Peta Interaktif', - 'login.features.mapsDesc': 'Google Places, rute & pengelompokan', - 'login.features.realtime': 'Sinkronisasi Real-Time', - 'login.features.realtimeDesc': 'Rencanakan bersama via WebSocket', - 'login.features.budget': 'Pelacak Biaya', - 'login.features.budgetDesc': 'Kategori, grafik & biaya per orang', - 'login.features.collab': 'Kolaborasi', - 'login.features.collabDesc': 'Multi-pengguna dengan perjalanan bersama', - 'login.features.packing': 'Daftar Packing', - 'login.features.packingDesc': 'Kategori, progres & saran', - 'login.features.bookings': 'Reservasi', - 'login.features.bookingsDesc': 'Penerbangan, hotel, restoran & lainnya', - 'login.features.files': 'Dokumen', - 'login.features.filesDesc': 'Unggah & kelola dokumen', - 'login.features.routes': 'Rute Cerdas', - 'login.features.routesDesc': 'Optimasi otomatis & ekspor ke Google Maps', - 'login.selfHosted': 'Self-hosted · Open Source · Datamu tetap milikmu', 'login.title': 'Masuk', 'login.subtitle': 'Selamat datang kembali', 'login.signingIn': 'Sedang masuk…', diff --git a/shared/src/i18n/it/login.ts b/shared/src/i18n/it/login.ts index 8a6ce0bd5..1804993f9 100644 --- a/shared/src/i18n/it/login.ts +++ b/shared/src/i18n/it/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'I tuoi viaggi.\nIl tuo programma.', 'login.description': 'Programma viaggi in collaborazione con mappe interattive, budget e sincronizzazione in tempo reale.', - 'login.features.maps': 'Mappe Interattive', - 'login.features.mapsDesc': 'Google Places, percorsi e clustering', - 'login.features.realtime': 'Sincronizzazione in tempo reale', - 'login.features.realtimeDesc': 'Programmate insieme tramite WebSocket', - 'login.features.budget': 'Tracciamento costi', - 'login.features.budgetDesc': 'Categorie, grafici e costi per persona', - 'login.features.collab': 'Collaborazione', - 'login.features.collabDesc': 'Multi-utente con viaggi condivisi', - 'login.features.packing': 'Lista Valigia', - 'login.features.packingDesc': 'Categorie, progressi e suggerimenti', - 'login.features.bookings': 'Prenotazioni', - 'login.features.bookingsDesc': 'Voli, alloggi, ristoranti e altro', - 'login.features.files': 'Documenti', - 'login.features.filesDesc': 'Carica e gestisci i documenti', - 'login.features.routes': 'Percorsi Intelligenti', - 'login.features.routesDesc': 'Ottimizzazione automatica ed esportazione su Google Maps', - 'login.selfHosted': 'Self-hosted · Open Source · Your data stays yours', 'login.title': 'Accedi', 'login.subtitle': 'Bentornato', 'login.signingIn': 'Accesso in corso…', diff --git a/shared/src/i18n/ja/login.ts b/shared/src/i18n/ja/login.ts index 238050912..6db123a19 100644 --- a/shared/src/i18n/ja/login.ts +++ b/shared/src/i18n/ja/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'ログインに失敗しました。認証情報を確認してください。', 'login.tagline': 'あなたの旅行。\nあなたの計画。', 'login.description': 'インタラクティブなマップ、予算、リアルタイム同期で、みんなで旅行を計画。', - 'login.features.maps': 'インタラクティブマップ', - 'login.features.mapsDesc': 'Google Places、経路、クラスタリング', - 'login.features.realtime': 'リアルタイム同期', - 'login.features.realtimeDesc': 'WebSocketで共同計画', - 'login.features.budget': '費用管理', - 'login.features.budgetDesc': 'カテゴリ、グラフ、人数別費用', - 'login.features.collab': 'コラボレーション', - 'login.features.collabDesc': '複数ユーザーで旅行を共有', - 'login.features.packing': '持ち物リスト', - 'login.features.packingDesc': 'カテゴリ、進捗、提案', - 'login.features.bookings': '予約', - 'login.features.bookingsDesc': '航空券、ホテル、レストランなど', - 'login.features.files': 'ドキュメント', - 'login.features.filesDesc': '書類のアップロードと管理', - 'login.features.routes': 'スマート経路', - 'login.features.routesDesc': '自動最適化&Google Maps書き出し', - 'login.selfHosted': 'セルフホスト · オープンソース · データはあなたのもの', 'login.title': 'サインイン', 'login.subtitle': 'おかえりなさい', 'login.signingIn': 'サインイン中…', diff --git a/shared/src/i18n/ko/login.ts b/shared/src/i18n/ko/login.ts index e2ed8cf42..45cf3685a 100644 --- a/shared/src/i18n/ko/login.ts +++ b/shared/src/i18n/ko/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': '로그인 실패. 자격 증명을 확인하세요.', 'login.tagline': '나의 여행.\n나의 계획.', 'login.description': '인터랙티브 지도, 예산, 실시간 동기화로 함께 여행을 계획하세요.', - 'login.features.maps': '인터랙티브 지도', - 'login.features.mapsDesc': 'Google Places, 경로 및 클러스터링', - 'login.features.realtime': '실시간 동기화', - 'login.features.realtimeDesc': 'WebSocket으로 함께 계획', - 'login.features.budget': '비용 추적', - 'login.features.budgetDesc': '카테고리, 차트 및 1인당 비용', - 'login.features.collab': '협업', - 'login.features.collabDesc': '공유 여행으로 다중 사용자 지원', - 'login.features.packing': '짐 목록', - 'login.features.packingDesc': '카테고리, 진행 상황 및 제안', - 'login.features.bookings': '예약', - 'login.features.bookingsDesc': '항공, 호텔, 레스토랑 등', - 'login.features.files': '문서', - 'login.features.filesDesc': '문서 업로드 및 관리', - 'login.features.routes': '스마트 경로', - 'login.features.routesDesc': '자동 최적화 및 Google Maps 내보내기', - 'login.selfHosted': '자체 호스팅 · 오픈 소스 · 내 데이터는 내 것', 'login.title': '로그인', 'login.subtitle': '다시 오신 것을 환영합니다', 'login.signingIn': '로그인 중…', diff --git a/shared/src/i18n/nl/login.ts b/shared/src/i18n/nl/login.ts index 00ac863eb..083106fa0 100644 --- a/shared/src/i18n/nl/login.ts +++ b/shared/src/i18n/nl/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Inloggen mislukt. Controleer je inloggegevens.', 'login.tagline': 'Jouw reizen.\nJouw plan.', 'login.description': 'Plan reizen samen met interactieve kaarten, budgetten en realtime synchronisatie.', - 'login.features.maps': 'Interactieve kaarten', - 'login.features.mapsDesc': 'Google Places, routes en clustering', - 'login.features.realtime': 'Realtime synchronisatie', - 'login.features.realtimeDesc': 'Plan samen via WebSocket', - 'login.features.budget': 'Onkostenbeheer', - 'login.features.budgetDesc': 'Categorieën, grafieken en kosten per persoon', - 'login.features.collab': 'Samenwerking', - 'login.features.collabDesc': 'Meerdere gebruikers met gedeelde reizen', - 'login.features.packing': 'Paklijsten', - 'login.features.packingDesc': 'Categorieën, voortgang en suggesties', - 'login.features.bookings': 'Reserveringen', - 'login.features.bookingsDesc': 'Vluchten, hotels, restaurants en meer', - 'login.features.files': 'Documenten', - 'login.features.filesDesc': 'Upload en beheer documenten', - 'login.features.routes': 'Slimme routes', - 'login.features.routesDesc': 'Automatisch optimaliseren en exporteren naar Google Maps', - 'login.selfHosted': 'Zelf gehost · Open Source · Jouw gegevens blijven van jou', 'login.title': 'Inloggen', 'login.subtitle': 'Welkom terug', 'login.signingIn': 'Inloggen…', diff --git a/shared/src/i18n/pl/login.ts b/shared/src/i18n/pl/login.ts index e98b9d106..a26d647ad 100644 --- a/shared/src/i18n/pl/login.ts +++ b/shared/src/i18n/pl/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Twoje podróże.\nTwój plan.', 'login.description': 'Planuj wspólnie podróże z interaktywnymi mapami, budżetami i synchronizacją w czasie rzeczywistym.', - 'login.features.maps': 'Interaktywne mapy', - 'login.features.mapsDesc': 'Google Places, trasy i grupowanie', - 'login.features.realtime': 'Synchronizacja w czasie rzeczywistym', - 'login.features.realtimeDesc': 'Planuj wspólnie przez WebSocket', - 'login.features.budget': 'Śledzenie kosztów', - 'login.features.budgetDesc': 'Kategorie, wykresy i koszty w przeliczeniu na osobę', - 'login.features.collab': 'Współpraca', - 'login.features.collabDesc': 'Wielu użytkowników i wspólne podróże', - 'login.features.packing': 'Listy pakowania', - 'login.features.packingDesc': 'Kategorie, postęp i sugestie', - 'login.features.bookings': 'Rezerwacje', - 'login.features.bookingsDesc': 'Loty, hotele, restauracje i więcej', - 'login.features.files': 'Dokumenty', - 'login.features.filesDesc': 'Przesyłaj i zarządzaj dokumentami', - 'login.features.routes': 'Inteligentne trasy', - 'login.features.routesDesc': 'Automatyczna optymalizacja i eksport do Google Maps', - 'login.selfHosted': 'Własny hosting · Otwarty kod źródłowy · Twoje dane pozostają Twoje', 'login.title': 'Zaloguj się', 'login.subtitle': 'Witaj ponownie', 'login.signingIn': 'Logowanie...', diff --git a/shared/src/i18n/ru/login.ts b/shared/src/i18n/ru/login.ts index cff0b25e2..48e7a1066 100644 --- a/shared/src/i18n/ru/login.ts +++ b/shared/src/i18n/ru/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Ваши поездки.\nВаш план.', 'login.description': 'Планируйте поездки совместно с интерактивными картами, бюджетами и синхронизацией в реальном времени.', - 'login.features.maps': 'Интерактивные карты', - 'login.features.mapsDesc': 'Google Places, маршруты и кластеризация', - 'login.features.realtime': 'Синхронизация в реальном времени', - 'login.features.realtimeDesc': 'Планируйте вместе через WebSocket', - 'login.features.budget': 'Контроль расходов', - 'login.features.budgetDesc': 'Категории, графики и расходы на человека', - 'login.features.collab': 'Совместная работа', - 'login.features.collabDesc': 'Многопользовательский режим с общими поездками', - 'login.features.packing': 'Списки вещей', - 'login.features.packingDesc': 'Категории, прогресс и подсказки', - 'login.features.bookings': 'Бронирования', - 'login.features.bookingsDesc': 'Авиабилеты, отели, рестораны и многое другое', - 'login.features.files': 'Документы', - 'login.features.filesDesc': 'Загружайте и управляйте документами', - 'login.features.routes': 'Умные маршруты', - 'login.features.routesDesc': 'Автооптимизация и экспорт в Google Maps', - 'login.selfHosted': 'Самостоятельный хостинг · Открытый код · Ваши данные остаются у вас', 'login.title': 'Вход', 'login.subtitle': 'С возвращением', 'login.signingIn': 'Вход…', diff --git a/shared/src/i18n/sv/login.ts b/shared/src/i18n/sv/login.ts index 09045dde8..05e0ec172 100644 --- a/shared/src/i18n/sv/login.ts +++ b/shared/src/i18n/sv/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Dina resor.\nDin plan.', 'login.description': 'Planera resor tillsammans med hjälp av interaktiva kartor, budgetar och synkronisering i realtid.', - 'login.features.maps': 'Interaktiva kartor', - 'login.features.mapsDesc': 'Google Places, rutter och klusterbildning', - 'login.features.realtime': 'Synkronisering i realtid', - 'login.features.realtimeDesc': 'Planera tillsammans via WebSocket', - 'login.features.budget': 'Kostnadsuppföljning', - 'login.features.budgetDesc': 'Kategorier, diagram och kostnader per person', - 'login.features.collab': 'Samarbete', - 'login.features.collabDesc': 'Flera användare med delade resor', - 'login.features.packing': 'Packlistor', - 'login.features.packingDesc': 'Kategorier, framsteg och förslag', - 'login.features.bookings': 'Bokningar', - 'login.features.bookingsDesc': 'Flyg, hotell, restauranger och mycket mer', - 'login.features.files': 'Dokument', - 'login.features.filesDesc': 'Ladda upp och hantera dokument', - 'login.features.routes': 'Smarta rutter', - 'login.features.routesDesc': 'Automatisk optimering och export till Google Maps', - 'login.selfHosted': 'Egenhostad · Öppen källkod · Dina data förblir dina', 'login.title': 'Logga in', 'login.subtitle': 'Välkommen tillbaka', 'login.signingIn': 'Loggar in…', diff --git a/shared/src/i18n/tr/login.ts b/shared/src/i18n/tr/login.ts index b6b11ec76..d851c51b9 100644 --- a/shared/src/i18n/tr/login.ts +++ b/shared/src/i18n/tr/login.ts @@ -5,23 +5,6 @@ const login: TranslationStrings = { 'login.tagline': 'Gezileriniz.\\nPlanınız.', 'login.description': 'Etkileşimli haritalar, bütçeler ve gerçek zamanlı senkronizasyonla gezileri işbirliği içinde planlayın.', - 'login.features.maps': 'İnteraktif Haritalar', - 'login.features.mapsDesc': 'Google Rehber, rotalar ve kümeleme', - 'login.features.realtime': 'Gerçek Zamanlı Senkronizasyon', - 'login.features.realtimeDesc': 'WebSocket aracılığıyla birlikte plan yapın', - 'login.features.budget': 'Maliyet Takibi', - 'login.features.budgetDesc': 'Kategoriler, grafikler ve kişi başı maliyetler', - 'login.features.collab': 'İşbirliği', - 'login.features.collabDesc': 'Paylaşılan gezilerle çok kullanıcılı', - 'login.features.packing': 'Paketleme Listeleri', - 'login.features.packingDesc': 'Kategoriler, ilerleme ve öneriler', - 'login.features.bookings': 'Rezervasyonlar', - 'login.features.bookingsDesc': 'Uçuşlar, oteller, restoranlar ve daha fazlası', - 'login.features.files': 'Belgeler', - 'login.features.filesDesc': 'Belgeleri yükleyin ve yönetin', - 'login.features.routes': 'Akıllı Rotalar', - 'login.features.routesDesc': "Otomatik optimize etme ve Google Haritalar'ı dışa aktarma", - 'login.selfHosted': 'Kendi kendine barındırılan \\u00B7 Açık Kaynak \\u00B7 Verileriniz size ait kalır', 'login.title': 'Oturum Aç', 'login.subtitle': 'Tekrar hoş geldiniz', 'login.signingIn': 'Oturum açılıyor…', diff --git a/shared/src/i18n/uk/login.ts b/shared/src/i18n/uk/login.ts index 741c5920f..ba502d03b 100644 --- a/shared/src/i18n/uk/login.ts +++ b/shared/src/i18n/uk/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Помилка входу. Перевірте свої облікові дані.', 'login.tagline': 'Ваші поїздки.\nВаш план.', 'login.description': 'Плануйте поїздки разом з інтерактивними картами, бюджетами та синхронізацією в реальному часі.', - 'login.features.maps': 'Інтерактивні карти', - 'login.features.mapsDesc': 'Google Places, маршрути та кластеризація', - 'login.features.realtime': 'Синхронізація в реальному часі', - 'login.features.realtimeDesc': 'Плануйте разом через WebSocket', - 'login.features.budget': 'Контроль витрат', - 'login.features.budgetDesc': 'Категорії, графіки та витрати на особу', - 'login.features.collab': 'Спільна робота', - 'login.features.collabDesc': 'Багатокористувацький режим зі спільними поїздками', - 'login.features.packing': 'Списки речей', - 'login.features.packingDesc': 'Категорії, прогрес та підказки', - 'login.features.bookings': 'Бронювання', - 'login.features.bookingsDesc': 'Авіаквитки, готелі, ресторани та багато іншого', - 'login.features.files': 'Документи', - 'login.features.filesDesc': 'Завантажуйте та керуйте документами', - 'login.features.routes': 'Розумні маршрути', - 'login.features.routesDesc': 'Автооптимізація та експорт з Google Maps', - 'login.selfHosted': 'Self-hosted · Відкритий код · Ваші дані залишаються у вас', 'login.title': 'Вхід', 'login.subtitle': 'З поверненням', 'login.signingIn': 'Вхід…', diff --git a/shared/src/i18n/vi/login.ts b/shared/src/i18n/vi/login.ts index 2fbd29e7f..495906834 100644 --- a/shared/src/i18n/vi/login.ts +++ b/shared/src/i18n/vi/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': 'Đăng nhập không thành công. Vui lòng kiểm tra thông tin đăng nhập của bạn.', 'login.tagline': 'Chuyến đi của bạn.\nKế hoạch của bạn.', 'login.description': 'Lập kế hoạch chuyến đi cộng tác với bản đồ tương tác, ngân sách và đồng bộ hóa thời gian thực.', - 'login.features.maps': 'Bản đồ tương tác', - 'login.features.mapsDesc': 'Google Địa điểm, tuyến đường và phân cụm', - 'login.features.realtime': 'Đồng bộ hóa thời gian thực', - 'login.features.realtimeDesc': 'Lập kế hoạch cùng nhau thông qua WebSocket', - 'login.features.budget': 'Theo dõi chi phí', - 'login.features.budgetDesc': 'Danh mục, biểu đồ và chi phí mỗi người', - 'login.features.collab': 'Sự hợp tác', - 'login.features.collabDesc': 'Nhiều người dùng với các chuyến đi chung', - 'login.features.packing': 'Danh sách đóng gói', - 'login.features.packingDesc': 'Danh mục, tiến độ và đề xuất', - 'login.features.bookings': 'Đặt chỗ', - 'login.features.bookingsDesc': 'Chuyến bay, khách sạn, nhà hàng và hơn thế nữa', - 'login.features.files': 'Tài liệu', - 'login.features.filesDesc': 'Tải lên và quản lý tài liệu', - 'login.features.routes': 'Tuyến đường thông minh', - 'login.features.routesDesc': 'Tự động tối ưu hóa & xuất Google Maps', - 'login.selfHosted': 'Tự lưu trữ · Mã nguồn mở · Dữ liệu của bạn vẫn là của bạn', 'login.title': 'Đăng nhập', 'login.subtitle': 'Chào mừng trở lại', 'login.signingIn': 'Đang đăng nhập…', diff --git a/shared/src/i18n/zh-TW/login.ts b/shared/src/i18n/zh-TW/login.ts index 099dbfe9c..22451f2cf 100644 --- a/shared/src/i18n/zh-TW/login.ts +++ b/shared/src/i18n/zh-TW/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': '登入失敗,請檢查你的憑據。', 'login.tagline': '你的旅行。\n你的計劃。', 'login.description': '透過互動地圖、預算管理和即時同步,協同規劃旅行。', - 'login.features.maps': '互動地圖', - 'login.features.mapsDesc': 'Google Places、路線和聚類', - 'login.features.realtime': '即時同步', - 'login.features.realtimeDesc': '透過 WebSocket 協同規劃', - 'login.features.budget': '費用追蹤', - 'login.features.budgetDesc': '分類、圖表和人均費用', - 'login.features.collab': '協作', - 'login.features.collabDesc': '多使用者共享旅行', - 'login.features.packing': '行李清單', - 'login.features.packingDesc': '分類、進度和建議', - 'login.features.bookings': '預訂', - 'login.features.bookingsDesc': '航班、酒店、餐廳等', - 'login.features.files': '文件', - 'login.features.filesDesc': '上傳和管理文件', - 'login.features.routes': '智慧路線', - 'login.features.routesDesc': '自動最佳化和匯出到 Google Maps', - 'login.selfHosted': '自託管 · 開源 · 資料由你掌控', 'login.title': '登入', 'login.subtitle': '歡迎回來', 'login.signingIn': '登入中…', diff --git a/shared/src/i18n/zh/login.ts b/shared/src/i18n/zh/login.ts index aacb4a1c7..02d7226ed 100644 --- a/shared/src/i18n/zh/login.ts +++ b/shared/src/i18n/zh/login.ts @@ -4,23 +4,6 @@ const login: TranslationStrings = { 'login.error': '登录失败,请检查你的凭据。', 'login.tagline': '你的旅行。\n你的计划。', 'login.description': '通过互动地图、预算管理和实时同步,协同规划旅行。', - 'login.features.maps': '互动地图', - 'login.features.mapsDesc': 'Google Places、路线和聚类', - 'login.features.realtime': '实时同步', - 'login.features.realtimeDesc': '通过 WebSocket 协同规划', - 'login.features.budget': '费用跟踪', - 'login.features.budgetDesc': '分类、图表和人均费用', - 'login.features.collab': '协作', - 'login.features.collabDesc': '多用户共享旅行', - 'login.features.packing': '行李清单', - 'login.features.packingDesc': '分类、进度和建议', - 'login.features.bookings': '预订', - 'login.features.bookingsDesc': '航班、酒店、餐厅等', - 'login.features.files': '文档', - 'login.features.filesDesc': '上传和管理文档', - 'login.features.routes': '智能路线', - 'login.features.routesDesc': '自动优化和导出到 Google Maps', - 'login.selfHosted': '自托管 · 开源 · 数据由你掌控', 'login.title': '登录', 'login.subtitle': '欢迎回来', 'login.signingIn': '登录中…', From 39d4dc267543651f5517802cbf7a9c3649d4a8f0 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 25 Jul 2026 14:42:18 +0200 Subject: [PATCH 5/5] feat(vacay): thin the mobile person card down to the number you act on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used, carried over, comp days and remaining all fit a desktop sidebar; on a phone they collapsed into four scraps of 9px text fighting for one row. Only the balance is left, as a pill tinted by how much of it there is — the rest is still a tap away in the entitlement view. The toolbar chip says "Company" rather than "Company Holiday": it sits beside two other buttons and the building icon already carries the meaning. The allowance stepper reads "Days" instead of "days / year", reusing the label the desktop entitlement tile has always used — the year is on the pill above it. --- client/src/mobile/screens/vacay/MVacay.tsx | 34 ++++++++++------------ shared/src/i18n/ar/mobileVacay.ts | 3 +- shared/src/i18n/br/mobileVacay.ts | 3 +- shared/src/i18n/ca/mobileVacay.ts | 3 +- shared/src/i18n/cs/mobileVacay.ts | 3 +- shared/src/i18n/de/mobileVacay.ts | 3 +- shared/src/i18n/en/mobileVacay.ts | 3 +- shared/src/i18n/es/mobileVacay.ts | 3 +- shared/src/i18n/fr/mobileVacay.ts | 3 +- shared/src/i18n/gr/mobileVacay.ts | 3 +- shared/src/i18n/hu/mobileVacay.ts | 3 +- shared/src/i18n/id/mobileVacay.ts | 3 +- shared/src/i18n/it/mobileVacay.ts | 3 +- shared/src/i18n/ja/mobileVacay.ts | 3 +- shared/src/i18n/ko/mobileVacay.ts | 3 +- shared/src/i18n/nl/mobileVacay.ts | 3 +- shared/src/i18n/pl/mobileVacay.ts | 3 +- shared/src/i18n/ru/mobileVacay.ts | 3 +- shared/src/i18n/sv/mobileVacay.ts | 3 +- shared/src/i18n/tr/mobileVacay.ts | 3 +- shared/src/i18n/uk/mobileVacay.ts | 3 +- shared/src/i18n/vi/mobileVacay.ts | 3 +- shared/src/i18n/zh-TW/mobileVacay.ts | 3 +- shared/src/i18n/zh/mobileVacay.ts | 3 +- 24 files changed, 61 insertions(+), 42 deletions(-) diff --git a/client/src/mobile/screens/vacay/MVacay.tsx b/client/src/mobile/screens/vacay/MVacay.tsx index ccff1b1e9..9d4074f1a 100644 --- a/client/src/mobile/screens/vacay/MVacay.tsx +++ b/client/src/mobile/screens/vacay/MVacay.tsx @@ -11,6 +11,9 @@ import MVacaySettingsSheet from './MVacaySettingsSheet' import MVacayShareSheet from './MVacayShareSheet' import { FALLBACK_PERSON_COLOR } from './vacayDayModel' +/** Half days (#552) make the balance fractional; one decimal is exact and never drifts. */ +const fmtDays = (n: number) => (Number.isInteger(n) ? String(n) : n.toFixed(1)) + const WEEKDAY_KEYS_MONDAY = ['vacay.mon', 'vacay.tue', 'vacay.wed', 'vacay.thu', 'vacay.fri', 'vacay.sat', 'vacay.sun'] as const const WEEKDAY_KEYS_SUNDAY = ['vacay.sun', 'vacay.mon', 'vacay.tue', 'vacay.wed', 'vacay.thu', 'vacay.fri', 'vacay.sat'] as const @@ -78,23 +81,16 @@ export default function MVacay() {
{v.selectedUser.username}
-
- - {t('mobileVacay.usedCount', { count: stat.used })} - - {stat.carried_over > 0 && ( - - +{stat.carried_over} {v.isShiftedYear ? t('vacay.carriedOverPrevPeriod') : t('vacay.carriedOver', { year: v.selectedYear - 1 })} - - )} - {/* Comp / flex days (#1074) sit outside used/left — they cost nothing. */} - {(stat.comp_used ?? 0) > 0 && ( - - {t('vacay.compUsedCount', { count: stat.comp_used })} - - )} - - {t('mobileVacay.leftCount', { count: stat.remaining })} + {/* Used, carried over and comp days all fit on a desktop sidebar; on a + phone they turn into four scraps of 9px text. Only the number you + act on stays — the rest is a tap away in the entitlement view. */} +
+ + {fmtDays(stat.remaining)} + {t('mobileVacay.left')}
@@ -110,7 +106,7 @@ export default function MVacay() {
{stat.vacation_days}
- {t('mobileVacay.daysPerYear')} + {t('vacay.entitlementDays')}
)} {/* Divider — comp/flex and half-day modify the selected person's logging, they aren't modes. */} diff --git a/shared/src/i18n/ar/mobileVacay.ts b/shared/src/i18n/ar/mobileVacay.ts index d232bd0c3..2ac769367 100644 --- a/shared/src/i18n/ar/mobileVacay.ts +++ b/shared/src/i18n/ar/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'الشهر التالي', 'mobileVacay.usedCount': '{count} مستخدَمة', 'mobileVacay.leftCount': '{count} متبقية', - 'mobileVacay.daysPerYear': 'يوم / سنة', + 'mobileVacay.left': 'متبقية', + 'mobileVacay.modeCompany': 'الشركة', 'mobileVacay.decreaseAllowance': 'تقليل الرصيد', 'mobileVacay.increaseAllowance': 'زيادة الرصيد', 'mobileVacay.companyLegend': 'أيام الشركة', diff --git a/shared/src/i18n/br/mobileVacay.ts b/shared/src/i18n/br/mobileVacay.ts index c97acf641..e35bdd9fa 100644 --- a/shared/src/i18n/br/mobileVacay.ts +++ b/shared/src/i18n/br/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Próximo mês', 'mobileVacay.usedCount': '{count} usados', 'mobileVacay.leftCount': '{count} restantes', - 'mobileVacay.daysPerYear': 'dias / ano', + 'mobileVacay.left': 'restantes', + 'mobileVacay.modeCompany': 'Empresa', 'mobileVacay.decreaseAllowance': 'Diminuir cota', 'mobileVacay.increaseAllowance': 'Aumentar cota', 'mobileVacay.companyLegend': 'Férias coletivas', diff --git a/shared/src/i18n/ca/mobileVacay.ts b/shared/src/i18n/ca/mobileVacay.ts index f8d6eda3c..8b11c010e 100644 --- a/shared/src/i18n/ca/mobileVacay.ts +++ b/shared/src/i18n/ca/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Mes següent', 'mobileVacay.usedCount': '{count} usats', 'mobileVacay.leftCount': '{count} restants', - 'mobileVacay.daysPerYear': 'dies / any', + 'mobileVacay.left': 'restants', + 'mobileVacay.modeCompany': 'Empresa', 'mobileVacay.decreaseAllowance': "Redueix l'assignació", 'mobileVacay.increaseAllowance': "Augmenta l'assignació", 'mobileVacay.companyLegend': "Dies d'empresa", diff --git a/shared/src/i18n/cs/mobileVacay.ts b/shared/src/i18n/cs/mobileVacay.ts index f2e377d41..d1d94ec9e 100644 --- a/shared/src/i18n/cs/mobileVacay.ts +++ b/shared/src/i18n/cs/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Další měsíc', 'mobileVacay.usedCount': '{count} využito', 'mobileVacay.leftCount': 'Zbývá {count}', - 'mobileVacay.daysPerYear': 'dní / rok', + 'mobileVacay.left': 'zbývá', + 'mobileVacay.modeCompany': 'Firma', 'mobileVacay.decreaseAllowance': 'Snížit nárok', 'mobileVacay.increaseAllowance': 'Zvýšit nárok', 'mobileVacay.companyLegend': 'Firemní dny', diff --git a/shared/src/i18n/de/mobileVacay.ts b/shared/src/i18n/de/mobileVacay.ts index 3b37b3349..c0aaabe40 100644 --- a/shared/src/i18n/de/mobileVacay.ts +++ b/shared/src/i18n/de/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Nächster Monat', 'mobileVacay.usedCount': '{count} genommen', 'mobileVacay.leftCount': '{count} übrig', - 'mobileVacay.daysPerYear': 'Tage / Jahr', + 'mobileVacay.left': 'übrig', + 'mobileVacay.modeCompany': 'Betrieb', 'mobileVacay.decreaseAllowance': 'Anspruch verringern', 'mobileVacay.increaseAllowance': 'Anspruch erhöhen', 'mobileVacay.companyLegend': 'Betriebsferien', diff --git a/shared/src/i18n/en/mobileVacay.ts b/shared/src/i18n/en/mobileVacay.ts index 797300603..5702daf5f 100644 --- a/shared/src/i18n/en/mobileVacay.ts +++ b/shared/src/i18n/en/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Next month', 'mobileVacay.usedCount': '{count} used', 'mobileVacay.leftCount': '{count} left', - 'mobileVacay.daysPerYear': 'days / year', + 'mobileVacay.left': 'left', + 'mobileVacay.modeCompany': 'Company', 'mobileVacay.decreaseAllowance': 'Decrease allowance', 'mobileVacay.increaseAllowance': 'Increase allowance', 'mobileVacay.companyLegend': 'Company days', diff --git a/shared/src/i18n/es/mobileVacay.ts b/shared/src/i18n/es/mobileVacay.ts index 3a331b4da..e0a4f3343 100644 --- a/shared/src/i18n/es/mobileVacay.ts +++ b/shared/src/i18n/es/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Mes siguiente', 'mobileVacay.usedCount': '{count} usados', 'mobileVacay.leftCount': '{count} restantes', - 'mobileVacay.daysPerYear': 'días / año', + 'mobileVacay.left': 'restantes', + 'mobileVacay.modeCompany': 'Empresa', 'mobileVacay.decreaseAllowance': 'Reducir asignación', 'mobileVacay.increaseAllowance': 'Aumentar asignación', 'mobileVacay.companyLegend': 'Días de empresa', diff --git a/shared/src/i18n/fr/mobileVacay.ts b/shared/src/i18n/fr/mobileVacay.ts index ab38484a5..ade2173c5 100644 --- a/shared/src/i18n/fr/mobileVacay.ts +++ b/shared/src/i18n/fr/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Mois suivant', 'mobileVacay.usedCount': '{count} pris', 'mobileVacay.leftCount': '{count} restants', - 'mobileVacay.daysPerYear': 'jours / an', + 'mobileVacay.left': 'restants', + 'mobileVacay.modeCompany': 'Entreprise', 'mobileVacay.decreaseAllowance': 'Diminuer le quota', 'mobileVacay.increaseAllowance': 'Augmenter le quota', 'mobileVacay.companyLegend': 'Congés d\'entreprise', diff --git a/shared/src/i18n/gr/mobileVacay.ts b/shared/src/i18n/gr/mobileVacay.ts index 2f08844f1..f806593b2 100644 --- a/shared/src/i18n/gr/mobileVacay.ts +++ b/shared/src/i18n/gr/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Επόμενος μήνας', 'mobileVacay.usedCount': '{count} σε χρήση', 'mobileVacay.leftCount': '{count} απομένουν', - 'mobileVacay.daysPerYear': 'ημέρες / έτος', + 'mobileVacay.left': 'απομένουν', + 'mobileVacay.modeCompany': 'Εταιρεία', 'mobileVacay.decreaseAllowance': 'Μείωση δικαιώματος', 'mobileVacay.increaseAllowance': 'Αύξηση δικαιώματος', 'mobileVacay.companyLegend': 'Άδεια εταιρείας', diff --git a/shared/src/i18n/hu/mobileVacay.ts b/shared/src/i18n/hu/mobileVacay.ts index 09038cc6e..7d4d044d0 100644 --- a/shared/src/i18n/hu/mobileVacay.ts +++ b/shared/src/i18n/hu/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Következő hónap', 'mobileVacay.usedCount': '{count} felhasználva', 'mobileVacay.leftCount': '{count} maradt', - 'mobileVacay.daysPerYear': 'nap / év', + 'mobileVacay.left': 'maradt', + 'mobileVacay.modeCompany': 'Céges', 'mobileVacay.decreaseAllowance': 'Keret csökkentése', 'mobileVacay.increaseAllowance': 'Keret növelése', 'mobileVacay.companyLegend': 'Céges szabadság', diff --git a/shared/src/i18n/id/mobileVacay.ts b/shared/src/i18n/id/mobileVacay.ts index 1acb914ed..42e2bc676 100644 --- a/shared/src/i18n/id/mobileVacay.ts +++ b/shared/src/i18n/id/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Bulan berikutnya', 'mobileVacay.usedCount': '{count} terpakai', 'mobileVacay.leftCount': '{count} tersisa', - 'mobileVacay.daysPerYear': 'hari / tahun', + 'mobileVacay.left': 'tersisa', + 'mobileVacay.modeCompany': 'Perusahaan', 'mobileVacay.decreaseAllowance': 'Kurangi jatah', 'mobileVacay.increaseAllowance': 'Tambah jatah', 'mobileVacay.companyLegend': 'Libur perusahaan', diff --git a/shared/src/i18n/it/mobileVacay.ts b/shared/src/i18n/it/mobileVacay.ts index 720500eaa..d1dda1431 100644 --- a/shared/src/i18n/it/mobileVacay.ts +++ b/shared/src/i18n/it/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Mese successivo', 'mobileVacay.usedCount': '{count} usati', 'mobileVacay.leftCount': '{count} rimanenti', - 'mobileVacay.daysPerYear': 'giorni / anno', + 'mobileVacay.left': 'rimanenti', + 'mobileVacay.modeCompany': 'Azienda', 'mobileVacay.decreaseAllowance': 'Riduci giorni disponibili', 'mobileVacay.increaseAllowance': 'Aumenta giorni disponibili', 'mobileVacay.companyLegend': 'Ferie aziendali', diff --git a/shared/src/i18n/ja/mobileVacay.ts b/shared/src/i18n/ja/mobileVacay.ts index 634081423..62d5d1732 100644 --- a/shared/src/i18n/ja/mobileVacay.ts +++ b/shared/src/i18n/ja/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': '次の月', 'mobileVacay.usedCount': '{count} 日取得', 'mobileVacay.leftCount': '残り {count} 日', - 'mobileVacay.daysPerYear': '日 / 年', + 'mobileVacay.left': '残り', + 'mobileVacay.modeCompany': '会社', 'mobileVacay.decreaseAllowance': '付与日数を減らす', 'mobileVacay.increaseAllowance': '付与日数を増やす', 'mobileVacay.companyLegend': '会社の休業日', diff --git a/shared/src/i18n/ko/mobileVacay.ts b/shared/src/i18n/ko/mobileVacay.ts index b4ccb2008..6a48063a0 100644 --- a/shared/src/i18n/ko/mobileVacay.ts +++ b/shared/src/i18n/ko/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': '다음 달', 'mobileVacay.usedCount': '{count}일 사용', 'mobileVacay.leftCount': '{count}일 남음', - 'mobileVacay.daysPerYear': '일 / 년', + 'mobileVacay.left': '남음', + 'mobileVacay.modeCompany': '회사', 'mobileVacay.decreaseAllowance': '할당 줄이기', 'mobileVacay.increaseAllowance': '할당 늘리기', 'mobileVacay.companyLegend': '회사 지정 휴무', diff --git a/shared/src/i18n/nl/mobileVacay.ts b/shared/src/i18n/nl/mobileVacay.ts index 03ae86de9..b3bca586c 100644 --- a/shared/src/i18n/nl/mobileVacay.ts +++ b/shared/src/i18n/nl/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Volgende maand', 'mobileVacay.usedCount': '{count} gebruikt', 'mobileVacay.leftCount': '{count} over', - 'mobileVacay.daysPerYear': 'dagen / jaar', + 'mobileVacay.left': 'over', + 'mobileVacay.modeCompany': 'Bedrijf', 'mobileVacay.decreaseAllowance': 'Tegoed verlagen', 'mobileVacay.increaseAllowance': 'Tegoed verhogen', 'mobileVacay.companyLegend': 'Bedrijfsverlof', diff --git a/shared/src/i18n/pl/mobileVacay.ts b/shared/src/i18n/pl/mobileVacay.ts index 9e252d784..77720119c 100644 --- a/shared/src/i18n/pl/mobileVacay.ts +++ b/shared/src/i18n/pl/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Następny miesiąc', 'mobileVacay.usedCount': 'Wykorzystano: {count}', 'mobileVacay.leftCount': 'Pozostało: {count}', - 'mobileVacay.daysPerYear': 'dni / rok', + 'mobileVacay.left': 'pozostało', + 'mobileVacay.modeCompany': 'Firma', 'mobileVacay.decreaseAllowance': 'Zmniejsz limit', 'mobileVacay.increaseAllowance': 'Zwiększ limit', 'mobileVacay.companyLegend': 'Dni firmowe', diff --git a/shared/src/i18n/ru/mobileVacay.ts b/shared/src/i18n/ru/mobileVacay.ts index cf5228129..6c5395ff3 100644 --- a/shared/src/i18n/ru/mobileVacay.ts +++ b/shared/src/i18n/ru/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Следующий месяц', 'mobileVacay.usedCount': 'Использовано: {count}', 'mobileVacay.leftCount': 'Осталось: {count}', - 'mobileVacay.daysPerYear': 'дней / год', + 'mobileVacay.left': 'осталось', + 'mobileVacay.modeCompany': 'Компания', 'mobileVacay.decreaseAllowance': 'Уменьшить лимит', 'mobileVacay.increaseAllowance': 'Увеличить лимит', 'mobileVacay.companyLegend': 'Корпоративные дни', diff --git a/shared/src/i18n/sv/mobileVacay.ts b/shared/src/i18n/sv/mobileVacay.ts index dd210c106..f5a607f52 100644 --- a/shared/src/i18n/sv/mobileVacay.ts +++ b/shared/src/i18n/sv/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Nästa månad', 'mobileVacay.usedCount': '{count} använda', 'mobileVacay.leftCount': '{count} kvar', - 'mobileVacay.daysPerYear': 'dagar / år', + 'mobileVacay.left': 'kvar', + 'mobileVacay.modeCompany': 'Företag', 'mobileVacay.decreaseAllowance': 'Minska kvot', 'mobileVacay.increaseAllowance': 'Öka kvot', 'mobileVacay.companyLegend': 'Företagsledighet', diff --git a/shared/src/i18n/tr/mobileVacay.ts b/shared/src/i18n/tr/mobileVacay.ts index c566a3adb..dc1bac367 100644 --- a/shared/src/i18n/tr/mobileVacay.ts +++ b/shared/src/i18n/tr/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Sonraki ay', 'mobileVacay.usedCount': '{count} kullanıldı', 'mobileVacay.leftCount': '{count} kaldı', - 'mobileVacay.daysPerYear': 'gün / yıl', + 'mobileVacay.left': 'kaldı', + 'mobileVacay.modeCompany': 'Şirket', 'mobileVacay.decreaseAllowance': 'Hakkı azalt', 'mobileVacay.increaseAllowance': 'Hakkı artır', 'mobileVacay.companyLegend': 'Şirket tatili', diff --git a/shared/src/i18n/uk/mobileVacay.ts b/shared/src/i18n/uk/mobileVacay.ts index 9f423eb42..57a95f9bb 100644 --- a/shared/src/i18n/uk/mobileVacay.ts +++ b/shared/src/i18n/uk/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Наступний місяць', 'mobileVacay.usedCount': 'Використано: {count}', 'mobileVacay.leftCount': 'Залишилось: {count}', - 'mobileVacay.daysPerYear': 'днів / рік', + 'mobileVacay.left': 'залишилось', + 'mobileVacay.modeCompany': 'Компанія', 'mobileVacay.decreaseAllowance': 'Зменшити ліміт', 'mobileVacay.increaseAllowance': 'Збільшити ліміт', 'mobileVacay.companyLegend': 'Корпоративні дні', diff --git a/shared/src/i18n/vi/mobileVacay.ts b/shared/src/i18n/vi/mobileVacay.ts index 0b3c39faf..bde6b7f24 100644 --- a/shared/src/i18n/vi/mobileVacay.ts +++ b/shared/src/i18n/vi/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': 'Tháng sau', 'mobileVacay.usedCount': 'Đã dùng {count}', 'mobileVacay.leftCount': 'Còn {count}', - 'mobileVacay.daysPerYear': 'ngày / năm', + 'mobileVacay.left': 'còn', + 'mobileVacay.modeCompany': 'Công ty', 'mobileVacay.decreaseAllowance': 'Giảm hạn mức', 'mobileVacay.increaseAllowance': 'Tăng hạn mức', 'mobileVacay.companyLegend': 'Ngày nghỉ công ty', diff --git a/shared/src/i18n/zh-TW/mobileVacay.ts b/shared/src/i18n/zh-TW/mobileVacay.ts index 5b98eb94e..ed6327a9c 100644 --- a/shared/src/i18n/zh-TW/mobileVacay.ts +++ b/shared/src/i18n/zh-TW/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': '下個月', 'mobileVacay.usedCount': '已用 {count}', 'mobileVacay.leftCount': '剩餘 {count}', - 'mobileVacay.daysPerYear': '天 / 年', + 'mobileVacay.left': '剩餘', + 'mobileVacay.modeCompany': '公司', 'mobileVacay.decreaseAllowance': '減少額度', 'mobileVacay.increaseAllowance': '增加額度', 'mobileVacay.companyLegend': '公司假期', diff --git a/shared/src/i18n/zh/mobileVacay.ts b/shared/src/i18n/zh/mobileVacay.ts index dd1a9645c..a8defcdbd 100644 --- a/shared/src/i18n/zh/mobileVacay.ts +++ b/shared/src/i18n/zh/mobileVacay.ts @@ -7,7 +7,8 @@ const mobileVacay: TranslationStrings = { 'mobileVacay.nextMonth': '下个月', 'mobileVacay.usedCount': '已用 {count}', 'mobileVacay.leftCount': '剩余 {count}', - 'mobileVacay.daysPerYear': '天 / 年', + 'mobileVacay.left': '剩余', + 'mobileVacay.modeCompany': '公司', 'mobileVacay.decreaseAllowance': '减少额度', 'mobileVacay.increaseAllowance': '增加额度', 'mobileVacay.companyLegend': '公司假期',