read.markets/app/templates/landing.html
Giorgio Gilestro ee8384f1ba i18n: bilingual landing page (EN / IT) with auto-detect routing
Public landing page is now served in English and Italian via path-
prefixed URLs (/en/ and /it/), with the bare / detecting the
visitor's language and 302-ing to the right one.

Routing
-------
* GET / : if authed → dashboard (unchanged). Otherwise the visitor's
  language is resolved from (in priority order) the rtm.lang cookie,
  the Accept-Language header, the cf-ipcountry geolocation header,
  and finally DEFAULT_LANG, then a 302 redirects to /<lang>/.
* GET /en/ + /it/ : render the localised landing template, set the
  rtm.lang cookie (1-year, SameSite=Lax) so the next /-visit goes
  straight to the same translation. Logged-in users with user.lang
  set bypass detection for / (same priority chain — user.lang wins
  on every public surface they touch).

Storage
-------
* app/locales/<lang>.yaml — flat-ish nested copy files. YAML chosen
  so a future translator can edit without touching Python. Strings
  containing inline HTML (<strong>, <em>, <a>) are rendered with the
  Jinja `safe` filter in the template.
* app/services/locales.py — loads at startup, exposes get_locale()
  and detect_public_lang(). Wraps each YAML tree in a small _Dotted
  view so templates can write {{ t.hero.subhead }} (deliberately NOT
  a dict subclass — dict's built-in method names would shadow YAML
  keys like `items`).

Template + chrome
-----------------
* landing.html ported in full to {{ t.<key> }} references.
* public_base.html gets <html lang="…"> + hreflang link tags (en/it/
  x-default) when a route opts in via lang_switch=true. A tiny
  EN | IT link group lands in the public header, only visible on
  surfaces that opt in.
* public.css picks up the small lang-switch widget styles.

Scope (intentionally narrow)
----------------------------
* Only the landing page is localised. Pricing, terms, privacy,
  disclaimer, login, verify all stay English-only for now; their
  header chrome stays English too because localising labels there
  while the linked content is still EN would be a worse mismatch.
* When other public pages get translated, lang_switch=true on those
  routes will surface the same widget there with no template changes.

Tests
-----
* tests/test_locales.py covers YAML load parity (every active
  language has a file), dotted access through the tree, the
  detection precedence chain, and the unknown-locale fallback.

Deps
----
* pyyaml was already in requirements.lock as a transitive but not
  declared. Added to pyproject so it stays pinned as an explicit
  direct dependency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 17:10:45 +02:00

148 lines
6 KiB
HTML

{% extends "public_base.html" %}
{% block title %}{{ BRAND_NAME }} &middot; {{ t.hero.tagline }}{% endblock %}
{% block main %}
<section class="hero">
<div class="hero__brand">{{ BRAND_NAME }}</div>
<h1 class="hero__headline">{{ t.hero.tagline }}</h1>
<p class="hero__subhead">{{ t.hero.subhead | safe }}</p>
<div class="hero__ctas">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-primary" href="/">{{ t.hero.cta_dashboard }}</a>
<a class="btn-secondary" href="/pricing">{{ t.hero.cta_pricing }}</a>
{% else %}
<a class="btn-primary" href="/login">{{ t.hero.cta_signup }}</a>
<a class="btn-secondary" href="/pricing">{{ t.hero.cta_pricing }}</a>
{% endif %}
</div>
</section>
<section class="shot-hero">
<button class="shot shot--hero"
data-full="{{ url_for('static', path='/images/dashboard.png') }}?v={{ ASSET_VERSION }}"
data-alt="{{ t.shot_dashboard.alt }}"
data-caption="{{ t.shot_dashboard.caption }}">
<img src="{{ url_for('static', path='/images/dashboard.png') }}?v={{ ASSET_VERSION }}"
alt="{{ t.shot_dashboard.alt }}" loading="lazy">
<span class="shot__zoom" aria-hidden="true">{{ t.shot_dashboard.zoom_hint }}</span>
</button>
</section>
<section class="feature-grid">
<div class="feature-card">
<div class="feature-card__tag">{{ t.features.news.tag }}</div>
<h3 class="feature-card__title">{{ t.features.news.title }}</h3>
<p class="feature-card__body">{{ t.features.news.body | safe }}</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/news-feed.png') }}?v={{ ASSET_VERSION }}"
data-alt="{{ t.features.news.shot_alt }}"
data-caption="{{ t.features.news.shot_caption }}">
<img src="{{ url_for('static', path='/images/news-feed.png') }}?v={{ ASSET_VERSION }}"
alt="{{ t.features.news.shot_alt }}" loading="lazy">
</button>
</div>
<div class="feature-card">
<div class="feature-card__tag">{{ t.features.indicators.tag }}</div>
<h3 class="feature-card__title">{{ t.features.indicators.title }}</h3>
<p class="feature-card__body">{{ t.features.indicators.body | safe }}</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/indicators-read.png') }}?v={{ ASSET_VERSION }}"
data-alt="{{ t.features.indicators.shot_alt }}"
data-caption="{{ t.features.indicators.shot_caption }}">
<img src="{{ url_for('static', path='/images/indicators-read.png') }}?v={{ ASSET_VERSION }}"
alt="{{ t.features.indicators.shot_alt }}" loading="lazy">
</button>
</div>
<div class="feature-card">
<div class="feature-card__tag">{{ t.features.strategic.tag }}</div>
<h3 class="feature-card__title">{{ t.features.strategic.title }}</h3>
<p class="feature-card__body">{{ t.features.strategic.body | safe }}</p>
<button class="shot feature-card__shot"
data-full="{{ url_for('static', path='/images/strategic-log.png') }}?v={{ ASSET_VERSION }}"
data-alt="{{ t.features.strategic.shot_alt }}"
data-caption="{{ t.features.strategic.shot_caption }}">
<img src="{{ url_for('static', path='/images/strategic-log.png') }}?v={{ ASSET_VERSION }}"
alt="{{ t.features.strategic.shot_alt }}" loading="lazy">
</button>
</div>
</section>
<section class="public-section" style="text-align:center;">
<p style="font-size: 14px; color: var(--muted); max-width: 60ch; margin: 0 auto;">
{{ t.multilang_callout | safe }}
</p>
</section>
<section class="public-section shots-section">
<h2 class="public-section__head">{{ t.more_views.head }}</h2>
<div class="shots-grid">
<button class="shot"
data-full="{{ url_for('static', path='/images/chat-with-log.png') }}?v={{ ASSET_VERSION }}"
data-alt="{{ t.more_views.chat.alt }}"
data-caption="{{ t.more_views.chat.caption }}">
<img src="{{ url_for('static', path='/images/chat-with-log.png') }}?v={{ ASSET_VERSION }}"
alt="{{ t.more_views.chat.alt }}" loading="lazy">
<div class="shot__caption">
<strong>{{ t.more_views.chat.caption_strong }}</strong>
<span>{{ t.more_views.chat.caption_span }}</span>
</div>
</button>
</div>
</section>
<section class="public-section">
<p style="font-size: 13.5px; color: var(--muted);">{{ t.portfolio_blurb | safe }}</p>
</section>
<section class="not-strip">
<strong>{{ t.not_strip.head }}</strong>
<ul>
{% for item in t.not_strip.items %}<li>{{ item }}</li>{% endfor %}
</ul>
</section>
<section class="public-section">
<p style="font-size: 13px; color: var(--muted);">{{ t.footer.legal | safe }}</p>
<div class="hero__ctas" style="margin-top:8px;">
{% if cu and (cu.user or cu.is_admin) %}
<a class="btn-primary" href="/">{{ t.hero.cta_dashboard }}</a>
{% else %}
<a class="btn-primary" href="/login">{{ t.hero.cta_signup }}</a>
{% endif %}
</div>
</section>
<dialog id="shot-modal" class="shot-modal" aria-label="Screenshot preview">
<button class="shot-modal__close" type="button" aria-label="Close">&times;</button>
<img id="shot-modal__img" alt="">
<p id="shot-modal__caption"></p>
</dialog>
<script>
(function () {
var dlg = document.getElementById('shot-modal');
var img = document.getElementById('shot-modal__img');
var cap = document.getElementById('shot-modal__caption');
if (!dlg || !dlg.showModal) return; // gracefully skip on ancient browsers
document.querySelectorAll('.shot').forEach(function (btn) {
btn.addEventListener('click', function () {
img.src = btn.dataset.full;
img.alt = btn.dataset.alt || '';
cap.textContent = btn.dataset.caption || '';
dlg.showModal();
});
});
// Backdrop click closes; clicking the image itself does not.
dlg.addEventListener('click', function (e) {
if (e.target === dlg) dlg.close();
});
dlg.querySelector('.shot-modal__close').addEventListener('click', function () {
dlg.close();
});
})();
</script>
{% endblock %}