From a5c800e87c6a469b1da449e0aa5fc6149ccb5865 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 13 Jul 2026 17:07:30 -0400 Subject: [PATCH] Jul 13 - Optimize code 2 --- .gitignore | 1 + CLAUDE.md | 42 ++++-- app/translations/es/LC_MESSAGES/messages.mo | Bin 0 -> 4560 bytes app/translations/es/LC_MESSAGES/messages.po | 150 ++++++++++---------- app/translations/vi/LC_MESSAGES/messages.mo | Bin 0 -> 4807 bytes app/translations/vi/LC_MESSAGES/messages.po | 150 ++++++++++---------- 6 files changed, 182 insertions(+), 161 deletions(-) create mode 100644 app/translations/es/LC_MESSAGES/messages.mo create mode 100644 app/translations/vi/LC_MESSAGES/messages.mo diff --git a/.gitignore b/.gitignore index 27b9281..e5af7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ __pycache__/ .env dev.db *.mo +!app/translations/**/*.mo instance/ .DS_Store diff --git a/CLAUDE.md b/CLAUDE.md index cff00f4..bb6a980 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,7 +42,7 @@ Each category has subcategories and a JSON-driven `field_schema` | Reverse proxy / static | Nginx | | Process manager | systemd | | OS | Ubuntu 22.04 Server | -| Background jobs | APScheduler or RQ (expiry sweep, email, image processing) | +| Background jobs | systemd timers (expiry/boost/keyword sweeps). RQ planned — not yet installed | | Payments | Stripe (Billing + Payment Intents + Tax + Customer Portal) | | Email | SMTP relay via Brevo (smart relay, existing infra) | | i18n | Flask-Babel | @@ -188,7 +188,8 @@ classifieds/ │ │ │ # moderator_required │ │ ├── security.py # hash_password, verify_password, generate_token, │ │ │ # read_token (itsdangerous) -│ │ └── text.py # normalize() — accent-insensitive (phở→pho, ñ→n) +│ │ ├── text.py # normalize() — accent-insensitive (phở→pho, ñ→n) +│ │ └── time.py # utcnow() — naive-UTC helper (replaces datetime.utcnow) │ │ │ ├── templates/ │ │ ├── base.html # Layout: responsive nav, OG/meta blocks, toast JS, @@ -710,6 +711,9 @@ Phase 6: flag threshold → auto-flip, keyword blocklist, reports queue, audit_l - Contact masking for low-trust users - Signed time-limited tokens for email verify + password reset - ProxyFix for correct `is_secure` + client-IP behind Nginx +- Mid-session ban enforcement: `enforce_active_account` before_request hook logs + out any authenticated user whose `status != active` on their next request + (Flask-Login only checks `is_active` at login time) - Stripe webhook signature verification on every event - Secrets via `.env` only — never committed - HTTPS only in prod; `SESSION_COOKIE_SECURE=True` in ProdConfig @@ -830,6 +834,8 @@ accounts. If `caching_sha2_password` errors: `pip install cryptography`. # 2. python3 -m venv venv && pip install -r requirements.txt # 3. cp .env.example .env && nano .env (no inline comments!) # 4. flask db upgrade && python seed.py +# 4b. Compile translations (committed .mo travel with the repo, but recompile +# after any .po change): pybabel compile -d app/translations # 5. Copy systemd units: cp deploy/classifieds.service /etc/systemd/system/ cp deploy/classifieds-expire.{service,timer} /etc/systemd/system/ @@ -848,7 +854,9 @@ certbot --nginx -d classifieds.ngodanguyen.tech Nginx config independently of DNS - DNS A record must point to server's public IP - `curl -4 ifconfig.me` vs `dig +short classifieds.ngodanguyen.tech` must match -- CSS cache busting: copy to `style.v2.css` and update `base.html` link +- CSS cache busting: bump the `?v=N` query string on the `style.css` link in + `base.html` (single `style.css` file; the old `style.vN.css` copy approach was + dropped) --- @@ -861,6 +869,9 @@ certbot --nginx -d classifieds.ngodanguyen.tech - All datetimes UTC in DB. Localize only at render. - Money: integer cents everywhere. Never `float` for currency. - `db.session.get(Model, pk)` not `Model.query.get(pk)` (SA 2.0 deprecated). +- UTC now: use `utcnow()` from `app/utils/time.py` — never `datetime.utcnow()` + (deprecated on Python 3.12+). The helper returns a naive UTC datetime to match + the naive `DateTime` columns; do not mix in tz-aware datetimes. - Enums: DB `ENUM` columns + Python `str, enum.Enum` in `models/enums.py`. - Validate `listings.attributes` against `field_schema` server-side on every save. - i18n: all user-facing strings in `_()`. No hardcoded English in templates. @@ -1063,9 +1074,12 @@ certbot --nginx -d classifieds.ngodanguyen.tech - [ ] Listing preview before publish **i18n completion** -- [ ] Extract all `_()` strings to `.pot` -- [ ] Translate VI + ES `.po` files -- [ ] Compile `.mo`, test all three locales +- [x] Translate VI + ES `.po` files (75-string Phase 1–2 catalog: auth + listings chrome) +- [x] Compile `.mo` for vi + es; verified loadable via gettext (placeholders + plural-forms intact) +- [x] `.mo` files un-gitignored so they deploy (see Known Issues) +- [ ] Re-extract `.pot` to capture Phase 3–7 strings (messaging/payments/admin) — + needs `pybabel extract` (Babel not currently installed); then translate + recompile +- [ ] Test all three locales end-to-end in the running app --- @@ -1115,11 +1129,17 @@ certbot --nginx -d classifieds.ngodanguyen.tech | `.env` inline comments | MUST NOT use `# comments` after values — python-dotenv does not strip them → `int()` ValueError on startup. | | Stripe webhook | `/billing/webhook` is `@csrf.exempt`. Always verify `Stripe-Signature` header first. | | MySQL user grants | Must create BOTH `@'localhost'` AND `@'127.0.0.1'` — MySQL treats them as different accounts. | -| CSS cache busting | Nginx serves static with 30d expires. To bust: copy to `style.vN.css` and update `base.html` link. | +| CSS cache busting | Nginx serves static with 30d expires. To bust: bump the `?v=N` query string on the `style.css` link in `base.html`. | +| Translations (.mo) | `*.mo` is gitignored **except** `app/translations/**` (negated) so compiled catalogs deploy with the code — the no-build-step deploy has no compile phase. Recompile with `pybabel compile -d app/translations` after editing any `.po`. | +| Python / datetime | Target 3.11–3.12 for prod parity. `datetime.utcnow()` is deprecated on 3.12+ — use `utcnow()` from `app/utils/time.py`. | --- -_End of spec. Phase 1–6 complete, all smoke-test checks green. -Phase 7 (Polish) in progress — SEO, UX, email notifications, and reviews done. -Remaining: metro landing pages, i18n .po compilation, Redis sessions, WebP thumbnails, -listing lightbox, load-more, "back to results", translation cache, weekly digest._ +_End of spec. Phase 1–6 complete. Phase 7 (Polish) in progress — SEO, UX, email +notifications, reviews, and VI/ES translation compilation done (existing catalog). +Remaining: metro landing pages, i18n re-extraction for Phase 3–7 strings, Redis +sessions, WebP thumbnails, listing lightbox, load-more, "back to results", +translation cache, weekly digest. +Note: run the smoke suite in a venv matching `requirements.txt` (Flask 3.0.3 / +Python 3.11–3.12); the ambient Python 3.14 env lacks `flask_login` and drifts from +the pins._ diff --git a/app/translations/es/LC_MESSAGES/messages.mo b/app/translations/es/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..8fef543fbd7681e2d1111a963a318b9cf9fc8bad GIT binary patch literal 4560 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 oR