Setting["address_en"] (Адмінка → Налаштування → Заклад), opt-in fallback
like Product.name_en — used in place of "address" for English-locale
customers in /api/settings (Mini App contacts + closed screen), the bot's
/contacts command, and the pickup-ready notification's address line.
Untranslated (blank) address_en falls back to the Ukrainian address as before.
Two gaps in the earlier i18n scaffolding: the Mini App's UA/EN switcher
existed but most screens and all menu content stayed hardcoded Ukrainian.
Menu content (business data, not UI chrome):
- Category.name_en, Product.name_en/description_en (nullable, opt-in per
item — untranslated items fall back to Ukrainian rather than showing
blank). Editable from the existing category/product admin forms.
- Allergen labels (a small fixed EU-mandated set) get an English label
directly in ALLERGENS, no DB column needed.
- /api/categories and /api/products take an optional ?locale= param (no
session on these unauthenticated routes to read a stored preference from).
Full EN coverage for everything else:
- Every remaining webapp screen: MenuPage/ProductCard, CartItemRow (fixes a
pre-existing gap in the "already translated" cart screen), OrdersPage,
ContactsPage, ClosedScreen, BottomNav, AnnouncementPopup, plus app-level
status/error text.
- Every reachable bot message: /menu, /contacts (including its
locale-dependent reply-keyboard button, matched by text across all
locales since the label itself varies), and the rating thank-you flow.
- Every customer-facing checkout/order error message (empty cart, closed,
min order amount, payment/promo validation, cancel/pay errors) via
locale params threaded through workhours.closed_message() and
promo.validate_promo_code().
New: PATCH /api/me/locale persists an explicit in-app language choice back
to TelegramUser.locale (fire-and-forget from the webapp switcher), so it
also applies to the bot's own push notifications, not just the Mini App
session that made the choice.
Fixed a real bug caught while testing: product_form.html's allergen
checklist unpacked ALLERGENS values as 2-tuples, which broke (ValueError)
once they became 3-tuples (uk_label, emoji, en_label) — admin's "add
product" page 500'd until this was caught in browser testing.
Backend: dependency-free t(key, locale) helper (bober_bbq/utils/i18n.py) in
the spirit of Setting.get(), a new TelegramUser.locale column (migrated via
the existing _add_column_if_missing pattern) populated from Telegram's
language_code on first contact, and order-status notification messages
(notify_customer.py) plus the bot's /start welcome translated to en as proof.
Webapp: a ~30-line I18nProvider/useI18n context (webapp/src/i18n/) with a
per-locale translations dict, auto-detecting locale from a saved choice,
Telegram's language_code, or the browser, with a manual UA/EN switcher in
the header. CartPage and CheckoutPage are fully bilingual end-to-end as the
representative slice; the rest of the app is unchanged Ukrainian-only.
Documented in docs/I18N.md: how to add strings/languages, and the explicit
boundary of what's translated today vs. left for later (admin panel is
intentionally untouched).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>