Jul 13 - Optimize code 2

This commit is contained in:
2026-07-13 17:07:30 -04:00
parent 00a9ba7770
commit a5c800e87c
6 changed files with 182 additions and 161 deletions
+1
View File
@@ -4,5 +4,6 @@ __pycache__/
.env .env
dev.db dev.db
*.mo *.mo
!app/translations/**/*.mo
instance/ instance/
.DS_Store .DS_Store
+31 -11
View File
@@ -42,7 +42,7 @@ Each category has subcategories and a JSON-driven `field_schema`
| Reverse proxy / static | Nginx | | Reverse proxy / static | Nginx |
| Process manager | systemd | | Process manager | systemd |
| OS | Ubuntu 22.04 Server | | 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) | | Payments | Stripe (Billing + Payment Intents + Tax + Customer Portal) |
| Email | SMTP relay via Brevo (smart relay, existing infra) | | Email | SMTP relay via Brevo (smart relay, existing infra) |
| i18n | Flask-Babel | | i18n | Flask-Babel |
@@ -188,7 +188,8 @@ classifieds/
│ │ │ # moderator_required │ │ │ # moderator_required
│ │ ├── security.py # hash_password, verify_password, generate_token, │ │ ├── security.py # hash_password, verify_password, generate_token,
│ │ │ # read_token (itsdangerous) │ │ │ # 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/ │ ├── templates/
│ │ ├── base.html # Layout: responsive nav, OG/meta blocks, toast JS, │ │ ├── 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 - Contact masking for low-trust users
- Signed time-limited tokens for email verify + password reset - Signed time-limited tokens for email verify + password reset
- ProxyFix for correct `is_secure` + client-IP behind Nginx - 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 - Stripe webhook signature verification on every event
- Secrets via `.env` only — never committed - Secrets via `.env` only — never committed
- HTTPS only in prod; `SESSION_COOKIE_SECURE=True` in ProdConfig - 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 # 2. python3 -m venv venv && pip install -r requirements.txt
# 3. cp .env.example .env && nano .env (no inline comments!) # 3. cp .env.example .env && nano .env (no inline comments!)
# 4. flask db upgrade && python seed.py # 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: # 5. Copy systemd units:
cp deploy/classifieds.service /etc/systemd/system/ cp deploy/classifieds.service /etc/systemd/system/
cp deploy/classifieds-expire.{service,timer} /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 Nginx config independently of DNS
- DNS A record must point to server's public IP - DNS A record must point to server's public IP
- `curl -4 ifconfig.me` vs `dig +short classifieds.ngodanguyen.tech` must match - `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. - All datetimes UTC in DB. Localize only at render.
- Money: integer cents everywhere. Never `float` for currency. - Money: integer cents everywhere. Never `float` for currency.
- `db.session.get(Model, pk)` not `Model.query.get(pk)` (SA 2.0 deprecated). - `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`. - Enums: DB `ENUM` columns + Python `str, enum.Enum` in `models/enums.py`.
- Validate `listings.attributes` against `field_schema` server-side on every save. - Validate `listings.attributes` against `field_schema` server-side on every save.
- i18n: all user-facing strings in `_()`. No hardcoded English in templates. - 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 - [ ] Listing preview before publish
**i18n completion** **i18n completion**
- [ ] Extract all `_()` strings to `.pot` - [x] Translate VI + ES `.po` files (75-string Phase 12 catalog: auth + listings chrome)
- [ ] Translate VI + ES `.po` files - [x] Compile `.mo` for vi + es; verified loadable via gettext (placeholders + plural-forms intact)
- [ ] Compile `.mo`, test all three locales - [x] `.mo` files un-gitignored so they deploy (see Known Issues)
- [ ] Re-extract `.pot` to capture Phase 37 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. | | `.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. | | 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. | | 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.113.12 for prod parity. `datetime.utcnow()` is deprecated on 3.12+ — use `utcnow()` from `app/utils/time.py`. |
--- ---
_End of spec. Phase 16 complete, all smoke-test checks green. _End of spec. Phase 16 complete. Phase 7 (Polish) in progress — SEO, UX, email
Phase 7 (Polish) in progress — SEO, UX, email notifications, and reviews done. notifications, reviews, and VI/ES translation compilation done (existing catalog).
Remaining: metro landing pages, i18n .po compilation, Redis sessions, WebP thumbnails, Remaining: metro landing pages, i18n re-extraction for Phase 37 strings, Redis
listing lightbox, load-more, "back to results", translation cache, weekly digest._ 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.113.12); the ambient Python 3.14 env lacks `flask_login` and drifts from
the pins._
Binary file not shown.
+75 -75
View File
@@ -20,313 +20,313 @@ msgstr ""
#: app/blueprints/auth/routes.py:28 #: app/blueprints/auth/routes.py:28
msgid "Verify your email" msgid "Verify your email"
msgstr "" msgstr "Verifica tu correo electrónico"
#: app/blueprints/auth/routes.py:29 #: app/blueprints/auth/routes.py:29
#, python-format #, python-format
msgid "Confirm your account: %(link)s" msgid "Confirm your account: %(link)s"
msgstr "" msgstr "Confirma tu cuenta: %(link)s"
#: app/blueprints/auth/routes.py:40 #: app/blueprints/auth/routes.py:40
msgid "Captcha verification failed." msgid "Captcha verification failed."
msgstr "" msgstr "La verificación de Captcha falló."
#: app/blueprints/auth/routes.py:45 #: app/blueprints/auth/routes.py:45
msgid "An account with that email already exists." msgid "An account with that email already exists."
msgstr "" msgstr "Ya existe una cuenta con ese correo electrónico."
#: app/blueprints/auth/routes.py:59 #: app/blueprints/auth/routes.py:59
msgid "Account created. Check your email to verify." msgid "Account created. Check your email to verify."
msgstr "" msgstr "Cuenta creada. Revisa tu correo para verificar."
#: app/blueprints/auth/routes.py:74 #: app/blueprints/auth/routes.py:74
msgid "Invalid email or password." msgid "Invalid email or password."
msgstr "" msgstr "Correo o contraseña no válidos."
#: app/blueprints/auth/routes.py:77 #: app/blueprints/auth/routes.py:77
msgid "This account is suspended." msgid "This account is suspended."
msgstr "" msgstr "Esta cuenta está suspendida."
#: app/blueprints/auth/routes.py:93 #: app/blueprints/auth/routes.py:93
msgid "Signed out." msgid "Signed out."
msgstr "" msgstr "Sesión cerrada."
#: app/blueprints/auth/routes.py:102 #: app/blueprints/auth/routes.py:102
msgid "Verification link is invalid or expired." msgid "Verification link is invalid or expired."
msgstr "" msgstr "El enlace de verificación no es válido o ha caducado."
#: app/blueprints/auth/routes.py:111 #: app/blueprints/auth/routes.py:111
msgid "Email verified. You're all set." msgid "Email verified. You're all set."
msgstr "" msgstr "Correo verificado. Todo listo."
#: app/blueprints/auth/routes.py:124 #: app/blueprints/auth/routes.py:124
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr "Restablece tu contraseña"
#: app/blueprints/auth/routes.py:125 #: app/blueprints/auth/routes.py:125
#, python-format #, python-format
msgid "Reset link: %(link)s" msgid "Reset link: %(link)s"
msgstr "" msgstr "Enlace de restablecimiento: %(link)s"
#: app/blueprints/auth/routes.py:127 #: app/blueprints/auth/routes.py:127
msgid "If that email exists, a reset link has been sent." msgid "If that email exists, a reset link has been sent."
msgstr "" msgstr "Si ese correo existe, se ha enviado un enlace de restablecimiento."
#: app/blueprints/auth/routes.py:137 #: app/blueprints/auth/routes.py:137
msgid "Reset link is invalid or expired." msgid "Reset link is invalid or expired."
msgstr "" msgstr "El enlace de restablecimiento no es válido o ha caducado."
#: app/blueprints/auth/routes.py:146 #: app/blueprints/auth/routes.py:146
msgid "Password updated. Sign in." msgid "Password updated. Sign in."
msgstr "" msgstr "Contraseña actualizada. Inicia sesión."
#: app/blueprints/listings/routes.py:72 #: app/blueprints/listings/routes.py:72
msgid "ZIP not found; showing all results." msgid "ZIP not found; showing all results."
msgstr "" msgstr "Código postal no encontrado; mostrando todos los resultados."
#: app/blueprints/listings/routes.py:103 #: app/blueprints/listings/routes.py:103
msgid "You've reached your plan's active-listing limit." msgid "You've reached your plan's active-listing limit."
msgstr "" msgstr "Has alcanzado el límite de anuncios activos de tu plan."
#: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171 #: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171
#, python-format #, python-format
msgid "%(f)s: %(m)s" msgid "%(f)s: %(m)s"
msgstr "" msgstr "%(f)s: %(m)s"
#: app/blueprints/listings/routes.py:133 #: app/blueprints/listings/routes.py:133
msgid "Listing published." msgid "Listing published."
msgstr "" msgstr "Anuncio publicado."
#: app/blueprints/listings/routes.py:176 #: app/blueprints/listings/routes.py:176
msgid "Listing updated." msgid "Listing updated."
msgstr "" msgstr "Anuncio actualizado."
#: app/blueprints/listings/routes.py:194 #: app/blueprints/listings/routes.py:194
msgid "Listing deleted." msgid "Listing deleted."
msgstr "" msgstr "Anuncio eliminado."
#: app/blueprints/listings/routes.py:207 #: app/blueprints/listings/routes.py:207
msgid "Marked as sold." msgid "Marked as sold."
msgstr "" msgstr "Marcado como vendido."
#: app/blueprints/listings/routes.py:236 #: app/blueprints/listings/routes.py:236
msgid "Photo removed." msgid "Photo removed."
msgstr "" msgstr "Foto eliminada."
#: app/blueprints/listings/routes.py:258 #: app/blueprints/listings/routes.py:258
#, python-format #, python-format
msgid "Photo limit (%(n)s) reached for your plan." msgid "Photo limit (%(n)s) reached for your plan."
msgstr "" msgstr "Se alcanzó el límite de fotos (%(n)s) de tu plan."
#: app/blueprints/listings/routes.py:265 #: app/blueprints/listings/routes.py:265
#, python-format #, python-format
msgid "Image skipped: %(m)s" msgid "Image skipped: %(m)s"
msgstr "" msgstr "Imagen omitida: %(m)s"
#: app/templates/base.html:15 #: app/templates/base.html:15
msgid "Browse" msgid "Browse"
msgstr "" msgstr "Explorar"
#: app/templates/base.html:17 #: app/templates/base.html:17
msgid "Post" msgid "Post"
msgstr "" msgstr "Publicar"
#: app/templates/base.html:18 app/templates/listings/mine.html:2 #: app/templates/base.html:18 app/templates/listings/mine.html:2
#: app/templates/listings/mine.html:6 #: app/templates/listings/mine.html:6
msgid "My listings" msgid "My listings"
msgstr "" msgstr "Mis anuncios"
#: app/templates/base.html:20 #: app/templates/base.html:20
msgid "Sign out" msgid "Sign out"
msgstr "" msgstr "Cerrar sesión"
#: app/templates/auth/login.html:3 app/templates/auth/login.html:6 #: app/templates/auth/login.html:3 app/templates/auth/login.html:6
#: app/templates/base.html:22 #: app/templates/base.html:22
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr "Iniciar sesión"
#: app/templates/auth/register.html:3 app/templates/base.html:23 #: app/templates/auth/register.html:3 app/templates/base.html:23
msgid "Register" msgid "Register"
msgstr "" msgstr "Registrarse"
#: app/templates/index.html:2 #: app/templates/index.html:2
msgid "Classifieds — Home" msgid "Classifieds — Home"
msgstr "" msgstr "Clasificados — Inicio"
#: app/templates/index.html:5 #: app/templates/index.html:5
msgid "Find what you need. Post what you offer." msgid "Find what you need. Post what you offer."
msgstr "" msgstr "Encuentra lo que necesitas. Publica lo que ofreces."
#: app/templates/index.html:6 #: app/templates/index.html:6
msgid "Buy, sell, request, hire, and connect across your community." msgid "Buy, sell, request, hire, and connect across your community."
msgstr "" msgstr "Compra, vende, solicita, contrata y conecta en tu comunidad."
#: app/templates/index.html:7 app/templates/listings/browse.html:2 #: app/templates/index.html:7 app/templates/listings/browse.html:2
msgid "Browse listings" msgid "Browse listings"
msgstr "" msgstr "Explorar anuncios"
#: app/templates/index.html:9 #: app/templates/index.html:9
msgid "Get started" msgid "Get started"
msgstr "" msgstr "Comenzar"
#: app/templates/auth/login.html:15 #: app/templates/auth/login.html:15
msgid "Forgot password?" msgid "Forgot password?"
msgstr "" msgstr "¿Olvidaste tu contraseña?"
#: app/templates/auth/login.html:16 app/templates/auth/register.html:6 #: app/templates/auth/login.html:16 app/templates/auth/register.html:6
msgid "Create account" msgid "Create account"
msgstr "" msgstr "Crear cuenta"
#: app/templates/auth/register.html:16 #: app/templates/auth/register.html:16
msgid "Already have an account? Sign in" msgid "Already have an account? Sign in"
msgstr "" msgstr "¿Ya tienes una cuenta? Inicia sesión"
#: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6 #: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6
msgid "Set new password" msgid "Set new password"
msgstr "" msgstr "Establecer nueva contraseña"
#: app/templates/auth/reset_request.html:3 #: app/templates/auth/reset_request.html:3
#: app/templates/auth/reset_request.html:6 #: app/templates/auth/reset_request.html:6
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr "Restablecer contraseña"
#: app/templates/errors/403.html:3 #: app/templates/errors/403.html:3
msgid "Forbidden — you do not have access." msgid "Forbidden — you do not have access."
msgstr "" msgstr "Prohibido: no tienes acceso."
#: app/templates/errors/403.html:3 app/templates/errors/404.html:3 #: app/templates/errors/403.html:3 app/templates/errors/404.html:3
#: app/templates/errors/500.html:3 #: app/templates/errors/500.html:3
msgid "Back home" msgid "Back home"
msgstr "" msgstr "Volver al inicio"
#: app/templates/errors/404.html:3 #: app/templates/errors/404.html:3
msgid "Not found." msgid "Not found."
msgstr "" msgstr "No encontrado."
#: app/templates/errors/500.html:3 #: app/templates/errors/500.html:3
msgid "Something went wrong." msgid "Something went wrong."
msgstr "" msgstr "Algo salió mal."
#: app/templates/listings/browse.html:6 #: app/templates/listings/browse.html:6
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Filtrar"
#: app/templates/listings/browse.html:9 #: app/templates/listings/browse.html:9
msgid "Keyword" msgid "Keyword"
msgstr "" msgstr "Palabra clave"
#: app/templates/listings/browse.html:13 #: app/templates/listings/browse.html:13
msgid "Category" msgid "Category"
msgstr "" msgstr "Categoría"
#: app/templates/listings/browse.html:15 #: app/templates/listings/browse.html:15
msgid "All" msgid "All"
msgstr "" msgstr "Todos"
#: app/templates/listings/browse.html:21 #: app/templates/listings/browse.html:21
msgid "State" msgid "State"
msgstr "" msgstr "Estado"
#: app/templates/listings/browse.html:24 #: app/templates/listings/browse.html:24
msgid "Min $" msgid "Min $"
msgstr "" msgstr "Mín $"
#: app/templates/listings/browse.html:26 #: app/templates/listings/browse.html:26
msgid "Max $" msgid "Max $"
msgstr "" msgstr "Máx $"
#: app/templates/listings/browse.html:30 #: app/templates/listings/browse.html:30
msgid "Near ZIP" msgid "Near ZIP"
msgstr "" msgstr "Cerca del código postal"
#: app/templates/listings/browse.html:32 #: app/templates/listings/browse.html:32
msgid "Radius (mi)" msgid "Radius (mi)"
msgstr "" msgstr "Radio (mi)"
#: app/templates/listings/browse.html:35 #: app/templates/listings/browse.html:35
msgid "Apply" msgid "Apply"
msgstr "" msgstr "Aplicar"
#: app/templates/listings/browse.html:40 #: app/templates/listings/browse.html:40
#, python-format #, python-format
msgid "%(n)s results within %(r)s mi of %(z)s" msgid "%(n)s results within %(r)s mi of %(z)s"
msgstr "" msgstr "%(n)s resultados dentro de %(r)s mi de %(z)s"
#: app/templates/listings/browse.html:41 #: app/templates/listings/browse.html:41
msgid "No listings found." msgid "No listings found."
msgstr "" msgstr "No se encontraron anuncios."
#: app/templates/listings/browse.html:46 #: app/templates/listings/browse.html:46
msgid "No photo" msgid "No photo"
msgstr "" msgstr "Sin foto"
#: app/templates/listings/browse.html:51 #: app/templates/listings/browse.html:51
msgid "Featured" msgid "Featured"
msgstr "" msgstr "Destacado"
#: app/templates/listings/browse.html:63 #: app/templates/listings/browse.html:63
msgid "Prev" msgid "Prev"
msgstr "" msgstr "Anterior"
#: app/templates/listings/browse.html:64 #: app/templates/listings/browse.html:64
msgid "Next" msgid "Next"
msgstr "" msgstr "Siguiente"
#: app/templates/listings/detail.html:14 #: app/templates/listings/detail.html:14
#, python-format #, python-format
msgid "%(n)s views" msgid "%(n)s views"
msgstr "" msgstr "%(n)s vistas"
#: app/templates/listings/detail.html:41 #: app/templates/listings/detail.html:41
msgid "Verified" msgid "Verified"
msgstr "" msgstr "Verificado"
#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19 #: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19
msgid "Edit" msgid "Edit"
msgstr "" msgstr "Editar"
#: app/templates/listings/detail.html:47 #: app/templates/listings/detail.html:47
msgid "Mark sold" msgid "Mark sold"
msgstr "" msgstr "Marcar vendido"
#: app/templates/listings/detail.html:51 #: app/templates/listings/detail.html:51
msgid "Delete this listing?" msgid "Delete this listing?"
msgstr "" msgstr "¿Eliminar este anuncio?"
#: app/templates/listings/detail.html:52 #: app/templates/listings/detail.html:52
msgid "Delete" msgid "Delete"
msgstr "" msgstr "Eliminar"
#: app/templates/listings/detail.html:55 #: app/templates/listings/detail.html:55
msgid "Messaging arrives in Phase 3." msgid "Messaging arrives in Phase 3."
msgstr "" msgstr "La mensajería llega en la Fase 3."
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 #: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "Edit listing" msgid "Edit listing"
msgstr "" msgstr "Editar anuncio"
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 #: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "New listing" msgid "New listing"
msgstr "" msgstr "Nuevo anuncio"
#: app/templates/listings/form.html:47 #: app/templates/listings/form.html:47
msgid "Current photos" msgid "Current photos"
msgstr "" msgstr "Fotos actuales"
#: app/templates/listings/mine.html:7 #: app/templates/listings/mine.html:7
#, python-format #, python-format
msgid "Active: %(a)s" msgid "Active: %(a)s"
msgstr "" msgstr "Activos: %(a)s"
#: app/templates/listings/mine.html:8 #: app/templates/listings/mine.html:8
msgid "Post new" msgid "Post new"
msgstr "" msgstr "Publicar nuevo"
#: app/templates/listings/mine.html:10 #: app/templates/listings/mine.html:10
msgid "No listings yet." msgid "No listings yet."
msgstr "" msgstr "Aún no hay anuncios."
#: app/templates/listings/mine.html:18 #: app/templates/listings/mine.html:18
msgid "views" msgid "views"
msgstr "" msgstr "vistas"
Binary file not shown.
+75 -75
View File
@@ -20,313 +20,313 @@ msgstr ""
#: app/blueprints/auth/routes.py:28 #: app/blueprints/auth/routes.py:28
msgid "Verify your email" msgid "Verify your email"
msgstr "" msgstr "Xác minh email của bạn"
#: app/blueprints/auth/routes.py:29 #: app/blueprints/auth/routes.py:29
#, python-format #, python-format
msgid "Confirm your account: %(link)s" msgid "Confirm your account: %(link)s"
msgstr "" msgstr "Xác nhận tài khoản của bạn: %(link)s"
#: app/blueprints/auth/routes.py:40 #: app/blueprints/auth/routes.py:40
msgid "Captcha verification failed." msgid "Captcha verification failed."
msgstr "" msgstr "Xác minh Captcha thất bại."
#: app/blueprints/auth/routes.py:45 #: app/blueprints/auth/routes.py:45
msgid "An account with that email already exists." msgid "An account with that email already exists."
msgstr "" msgstr "Đã tồn tại tài khoản với email đó."
#: app/blueprints/auth/routes.py:59 #: app/blueprints/auth/routes.py:59
msgid "Account created. Check your email to verify." msgid "Account created. Check your email to verify."
msgstr "" msgstr "Đã tạo tài khoản. Kiểm tra email để xác minh."
#: app/blueprints/auth/routes.py:74 #: app/blueprints/auth/routes.py:74
msgid "Invalid email or password." msgid "Invalid email or password."
msgstr "" msgstr "Email hoặc mật khẩu không hợp lệ."
#: app/blueprints/auth/routes.py:77 #: app/blueprints/auth/routes.py:77
msgid "This account is suspended." msgid "This account is suspended."
msgstr "" msgstr "Tài khoản này đã bị tạm khóa."
#: app/blueprints/auth/routes.py:93 #: app/blueprints/auth/routes.py:93
msgid "Signed out." msgid "Signed out."
msgstr "" msgstr "Đã đăng xuất."
#: app/blueprints/auth/routes.py:102 #: app/blueprints/auth/routes.py:102
msgid "Verification link is invalid or expired." msgid "Verification link is invalid or expired."
msgstr "" msgstr "Liên kết xác minh không hợp lệ hoặc đã hết hạn."
#: app/blueprints/auth/routes.py:111 #: app/blueprints/auth/routes.py:111
msgid "Email verified. You're all set." msgid "Email verified. You're all set."
msgstr "" msgstr "Đã xác minh email. Bạn đã sẵn sàng."
#: app/blueprints/auth/routes.py:124 #: app/blueprints/auth/routes.py:124
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr "Đặt lại mật khẩu của bạn"
#: app/blueprints/auth/routes.py:125 #: app/blueprints/auth/routes.py:125
#, python-format #, python-format
msgid "Reset link: %(link)s" msgid "Reset link: %(link)s"
msgstr "" msgstr "Liên kết đặt lại: %(link)s"
#: app/blueprints/auth/routes.py:127 #: app/blueprints/auth/routes.py:127
msgid "If that email exists, a reset link has been sent." msgid "If that email exists, a reset link has been sent."
msgstr "" msgstr "Nếu email đó tồn tại, liên kết đặt lại đã được gửi."
#: app/blueprints/auth/routes.py:137 #: app/blueprints/auth/routes.py:137
msgid "Reset link is invalid or expired." msgid "Reset link is invalid or expired."
msgstr "" msgstr "Liên kết đặt lại không hợp lệ hoặc đã hết hạn."
#: app/blueprints/auth/routes.py:146 #: app/blueprints/auth/routes.py:146
msgid "Password updated. Sign in." msgid "Password updated. Sign in."
msgstr "" msgstr "Đã cập nhật mật khẩu. Đăng nhập."
#: app/blueprints/listings/routes.py:72 #: app/blueprints/listings/routes.py:72
msgid "ZIP not found; showing all results." msgid "ZIP not found; showing all results."
msgstr "" msgstr "Không tìm thấy mã ZIP; hiển thị tất cả kết quả."
#: app/blueprints/listings/routes.py:103 #: app/blueprints/listings/routes.py:103
msgid "You've reached your plan's active-listing limit." msgid "You've reached your plan's active-listing limit."
msgstr "" msgstr "Bạn đã đạt đến giới hạn tin đang đăng của gói."
#: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171 #: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171
#, python-format #, python-format
msgid "%(f)s: %(m)s" msgid "%(f)s: %(m)s"
msgstr "" msgstr "%(f)s: %(m)s"
#: app/blueprints/listings/routes.py:133 #: app/blueprints/listings/routes.py:133
msgid "Listing published." msgid "Listing published."
msgstr "" msgstr "Đã đăng tin."
#: app/blueprints/listings/routes.py:176 #: app/blueprints/listings/routes.py:176
msgid "Listing updated." msgid "Listing updated."
msgstr "" msgstr "Đã cập nhật tin."
#: app/blueprints/listings/routes.py:194 #: app/blueprints/listings/routes.py:194
msgid "Listing deleted." msgid "Listing deleted."
msgstr "" msgstr "Đã xóa tin."
#: app/blueprints/listings/routes.py:207 #: app/blueprints/listings/routes.py:207
msgid "Marked as sold." msgid "Marked as sold."
msgstr "" msgstr "Đã đánh dấu là đã bán."
#: app/blueprints/listings/routes.py:236 #: app/blueprints/listings/routes.py:236
msgid "Photo removed." msgid "Photo removed."
msgstr "" msgstr "Đã xóa ảnh."
#: app/blueprints/listings/routes.py:258 #: app/blueprints/listings/routes.py:258
#, python-format #, python-format
msgid "Photo limit (%(n)s) reached for your plan." msgid "Photo limit (%(n)s) reached for your plan."
msgstr "" msgstr "Đã đạt giới hạn ảnh (%(n)s) của gói."
#: app/blueprints/listings/routes.py:265 #: app/blueprints/listings/routes.py:265
#, python-format #, python-format
msgid "Image skipped: %(m)s" msgid "Image skipped: %(m)s"
msgstr "" msgstr "Đã bỏ qua ảnh: %(m)s"
#: app/templates/base.html:15 #: app/templates/base.html:15
msgid "Browse" msgid "Browse"
msgstr "" msgstr "Duyệt"
#: app/templates/base.html:17 #: app/templates/base.html:17
msgid "Post" msgid "Post"
msgstr "" msgstr "Đăng tin"
#: app/templates/base.html:18 app/templates/listings/mine.html:2 #: app/templates/base.html:18 app/templates/listings/mine.html:2
#: app/templates/listings/mine.html:6 #: app/templates/listings/mine.html:6
msgid "My listings" msgid "My listings"
msgstr "" msgstr "Tin của tôi"
#: app/templates/base.html:20 #: app/templates/base.html:20
msgid "Sign out" msgid "Sign out"
msgstr "" msgstr "Đăng xuất"
#: app/templates/auth/login.html:3 app/templates/auth/login.html:6 #: app/templates/auth/login.html:3 app/templates/auth/login.html:6
#: app/templates/base.html:22 #: app/templates/base.html:22
msgid "Sign in" msgid "Sign in"
msgstr "" msgstr "Đăng nhập"
#: app/templates/auth/register.html:3 app/templates/base.html:23 #: app/templates/auth/register.html:3 app/templates/base.html:23
msgid "Register" msgid "Register"
msgstr "" msgstr "Đăng ký"
#: app/templates/index.html:2 #: app/templates/index.html:2
msgid "Classifieds — Home" msgid "Classifieds — Home"
msgstr "" msgstr "Rao vặt — Trang chủ"
#: app/templates/index.html:5 #: app/templates/index.html:5
msgid "Find what you need. Post what you offer." msgid "Find what you need. Post what you offer."
msgstr "" msgstr "Tìm thứ bạn cần. Đăng thứ bạn có."
#: app/templates/index.html:6 #: app/templates/index.html:6
msgid "Buy, sell, request, hire, and connect across your community." msgid "Buy, sell, request, hire, and connect across your community."
msgstr "" msgstr "Mua, bán, yêu cầu, tuyển dụng và kết nối trong cộng đồng của bạn."
#: app/templates/index.html:7 app/templates/listings/browse.html:2 #: app/templates/index.html:7 app/templates/listings/browse.html:2
msgid "Browse listings" msgid "Browse listings"
msgstr "" msgstr "Duyệt tin đăng"
#: app/templates/index.html:9 #: app/templates/index.html:9
msgid "Get started" msgid "Get started"
msgstr "" msgstr "Bắt đầu"
#: app/templates/auth/login.html:15 #: app/templates/auth/login.html:15
msgid "Forgot password?" msgid "Forgot password?"
msgstr "" msgstr "Quên mật khẩu?"
#: app/templates/auth/login.html:16 app/templates/auth/register.html:6 #: app/templates/auth/login.html:16 app/templates/auth/register.html:6
msgid "Create account" msgid "Create account"
msgstr "" msgstr "Tạo tài khoản"
#: app/templates/auth/register.html:16 #: app/templates/auth/register.html:16
msgid "Already have an account? Sign in" msgid "Already have an account? Sign in"
msgstr "" msgstr "Đã có tài khoản? Đăng nhập"
#: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6 #: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6
msgid "Set new password" msgid "Set new password"
msgstr "" msgstr "Đặt mật khẩu mới"
#: app/templates/auth/reset_request.html:3 #: app/templates/auth/reset_request.html:3
#: app/templates/auth/reset_request.html:6 #: app/templates/auth/reset_request.html:6
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr "Đặt lại mật khẩu"
#: app/templates/errors/403.html:3 #: app/templates/errors/403.html:3
msgid "Forbidden — you do not have access." msgid "Forbidden — you do not have access."
msgstr "" msgstr "Bị cấm — bạn không có quyền truy cập."
#: app/templates/errors/403.html:3 app/templates/errors/404.html:3 #: app/templates/errors/403.html:3 app/templates/errors/404.html:3
#: app/templates/errors/500.html:3 #: app/templates/errors/500.html:3
msgid "Back home" msgid "Back home"
msgstr "" msgstr "Về trang chủ"
#: app/templates/errors/404.html:3 #: app/templates/errors/404.html:3
msgid "Not found." msgid "Not found."
msgstr "" msgstr "Không tìm thấy."
#: app/templates/errors/500.html:3 #: app/templates/errors/500.html:3
msgid "Something went wrong." msgid "Something went wrong."
msgstr "" msgstr "Đã xảy ra lỗi."
#: app/templates/listings/browse.html:6 #: app/templates/listings/browse.html:6
msgid "Filter" msgid "Filter"
msgstr "" msgstr "Lọc"
#: app/templates/listings/browse.html:9 #: app/templates/listings/browse.html:9
msgid "Keyword" msgid "Keyword"
msgstr "" msgstr "Từ khóa"
#: app/templates/listings/browse.html:13 #: app/templates/listings/browse.html:13
msgid "Category" msgid "Category"
msgstr "" msgstr "Danh mục"
#: app/templates/listings/browse.html:15 #: app/templates/listings/browse.html:15
msgid "All" msgid "All"
msgstr "" msgstr "Tất cả"
#: app/templates/listings/browse.html:21 #: app/templates/listings/browse.html:21
msgid "State" msgid "State"
msgstr "" msgstr "Tiểu bang"
#: app/templates/listings/browse.html:24 #: app/templates/listings/browse.html:24
msgid "Min $" msgid "Min $"
msgstr "" msgstr "Giá tối thiểu $"
#: app/templates/listings/browse.html:26 #: app/templates/listings/browse.html:26
msgid "Max $" msgid "Max $"
msgstr "" msgstr "Giá tối đa $"
#: app/templates/listings/browse.html:30 #: app/templates/listings/browse.html:30
msgid "Near ZIP" msgid "Near ZIP"
msgstr "" msgstr "Gần mã ZIP"
#: app/templates/listings/browse.html:32 #: app/templates/listings/browse.html:32
msgid "Radius (mi)" msgid "Radius (mi)"
msgstr "" msgstr "Bán kính (dặm)"
#: app/templates/listings/browse.html:35 #: app/templates/listings/browse.html:35
msgid "Apply" msgid "Apply"
msgstr "" msgstr "Áp dụng"
#: app/templates/listings/browse.html:40 #: app/templates/listings/browse.html:40
#, python-format #, python-format
msgid "%(n)s results within %(r)s mi of %(z)s" msgid "%(n)s results within %(r)s mi of %(z)s"
msgstr "" msgstr "%(n)s kết quả trong vòng %(r)s dặm quanh %(z)s"
#: app/templates/listings/browse.html:41 #: app/templates/listings/browse.html:41
msgid "No listings found." msgid "No listings found."
msgstr "" msgstr "Không tìm thấy tin đăng."
#: app/templates/listings/browse.html:46 #: app/templates/listings/browse.html:46
msgid "No photo" msgid "No photo"
msgstr "" msgstr "Không có ảnh"
#: app/templates/listings/browse.html:51 #: app/templates/listings/browse.html:51
msgid "Featured" msgid "Featured"
msgstr "" msgstr "Nổi bật"
#: app/templates/listings/browse.html:63 #: app/templates/listings/browse.html:63
msgid "Prev" msgid "Prev"
msgstr "" msgstr "Trước"
#: app/templates/listings/browse.html:64 #: app/templates/listings/browse.html:64
msgid "Next" msgid "Next"
msgstr "" msgstr "Sau"
#: app/templates/listings/detail.html:14 #: app/templates/listings/detail.html:14
#, python-format #, python-format
msgid "%(n)s views" msgid "%(n)s views"
msgstr "" msgstr "%(n)s lượt xem"
#: app/templates/listings/detail.html:41 #: app/templates/listings/detail.html:41
msgid "Verified" msgid "Verified"
msgstr "" msgstr "Đã xác minh"
#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19 #: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19
msgid "Edit" msgid "Edit"
msgstr "" msgstr "Sửa"
#: app/templates/listings/detail.html:47 #: app/templates/listings/detail.html:47
msgid "Mark sold" msgid "Mark sold"
msgstr "" msgstr "Đánh dấu đã bán"
#: app/templates/listings/detail.html:51 #: app/templates/listings/detail.html:51
msgid "Delete this listing?" msgid "Delete this listing?"
msgstr "" msgstr "Xóa tin này?"
#: app/templates/listings/detail.html:52 #: app/templates/listings/detail.html:52
msgid "Delete" msgid "Delete"
msgstr "" msgstr "Xóa"
#: app/templates/listings/detail.html:55 #: app/templates/listings/detail.html:55
msgid "Messaging arrives in Phase 3." msgid "Messaging arrives in Phase 3."
msgstr "" msgstr "Tính năng nhắn tin sẽ có ở Giai đoạn 3."
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 #: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "Edit listing" msgid "Edit listing"
msgstr "" msgstr "Sửa tin"
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 #: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "New listing" msgid "New listing"
msgstr "" msgstr "Tin mới"
#: app/templates/listings/form.html:47 #: app/templates/listings/form.html:47
msgid "Current photos" msgid "Current photos"
msgstr "" msgstr "Ảnh hiện tại"
#: app/templates/listings/mine.html:7 #: app/templates/listings/mine.html:7
#, python-format #, python-format
msgid "Active: %(a)s" msgid "Active: %(a)s"
msgstr "" msgstr "Đang đăng: %(a)s"
#: app/templates/listings/mine.html:8 #: app/templates/listings/mine.html:8
msgid "Post new" msgid "Post new"
msgstr "" msgstr "Đăng tin mới"
#: app/templates/listings/mine.html:10 #: app/templates/listings/mine.html:10
msgid "No listings yet." msgid "No listings yet."
msgstr "" msgstr "Chưa có tin đăng."
#: app/templates/listings/mine.html:18 #: app/templates/listings/mine.html:18
msgid "views" msgid "views"
msgstr "" msgstr "lượt xem"