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
dev.db
*.mo
!app/translations/**/*.mo
instance/
.DS_Store
+31 -11
View File
@@ -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 12 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 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. |
| 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.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.
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 16 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 37 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.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
msgid "Verify your email"
msgstr ""
msgstr "Verifica tu correo electrónico"
#: app/blueprints/auth/routes.py:29
#, python-format
msgid "Confirm your account: %(link)s"
msgstr ""
msgstr "Confirma tu cuenta: %(link)s"
#: app/blueprints/auth/routes.py:40
msgid "Captcha verification failed."
msgstr ""
msgstr "La verificación de Captcha falló."
#: app/blueprints/auth/routes.py:45
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
msgid "Account created. Check your email to verify."
msgstr ""
msgstr "Cuenta creada. Revisa tu correo para verificar."
#: app/blueprints/auth/routes.py:74
msgid "Invalid email or password."
msgstr ""
msgstr "Correo o contraseña no válidos."
#: app/blueprints/auth/routes.py:77
msgid "This account is suspended."
msgstr ""
msgstr "Esta cuenta está suspendida."
#: app/blueprints/auth/routes.py:93
msgid "Signed out."
msgstr ""
msgstr "Sesión cerrada."
#: app/blueprints/auth/routes.py:102
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
msgid "Email verified. You're all set."
msgstr ""
msgstr "Correo verificado. Todo listo."
#: app/blueprints/auth/routes.py:124
msgid "Reset your password"
msgstr ""
msgstr "Restablece tu contraseña"
#: app/blueprints/auth/routes.py:125
#, python-format
msgid "Reset link: %(link)s"
msgstr ""
msgstr "Enlace de restablecimiento: %(link)s"
#: app/blueprints/auth/routes.py:127
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
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
msgid "Password updated. Sign in."
msgstr ""
msgstr "Contraseña actualizada. Inicia sesión."
#: app/blueprints/listings/routes.py:72
msgid "ZIP not found; showing all results."
msgstr ""
msgstr "Código postal no encontrado; mostrando todos los resultados."
#: app/blueprints/listings/routes.py:103
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
#, python-format
msgid "%(f)s: %(m)s"
msgstr ""
msgstr "%(f)s: %(m)s"
#: app/blueprints/listings/routes.py:133
msgid "Listing published."
msgstr ""
msgstr "Anuncio publicado."
#: app/blueprints/listings/routes.py:176
msgid "Listing updated."
msgstr ""
msgstr "Anuncio actualizado."
#: app/blueprints/listings/routes.py:194
msgid "Listing deleted."
msgstr ""
msgstr "Anuncio eliminado."
#: app/blueprints/listings/routes.py:207
msgid "Marked as sold."
msgstr ""
msgstr "Marcado como vendido."
#: app/blueprints/listings/routes.py:236
msgid "Photo removed."
msgstr ""
msgstr "Foto eliminada."
#: app/blueprints/listings/routes.py:258
#, python-format
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
#, python-format
msgid "Image skipped: %(m)s"
msgstr ""
msgstr "Imagen omitida: %(m)s"
#: app/templates/base.html:15
msgid "Browse"
msgstr ""
msgstr "Explorar"
#: app/templates/base.html:17
msgid "Post"
msgstr ""
msgstr "Publicar"
#: app/templates/base.html:18 app/templates/listings/mine.html:2
#: app/templates/listings/mine.html:6
msgid "My listings"
msgstr ""
msgstr "Mis anuncios"
#: app/templates/base.html:20
msgid "Sign out"
msgstr ""
msgstr "Cerrar sesión"
#: app/templates/auth/login.html:3 app/templates/auth/login.html:6
#: app/templates/base.html:22
msgid "Sign in"
msgstr ""
msgstr "Iniciar sesión"
#: app/templates/auth/register.html:3 app/templates/base.html:23
msgid "Register"
msgstr ""
msgstr "Registrarse"
#: app/templates/index.html:2
msgid "Classifieds — Home"
msgstr ""
msgstr "Clasificados — Inicio"
#: app/templates/index.html:5
msgid "Find what you need. Post what you offer."
msgstr ""
msgstr "Encuentra lo que necesitas. Publica lo que ofreces."
#: app/templates/index.html:6
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
msgid "Browse listings"
msgstr ""
msgstr "Explorar anuncios"
#: app/templates/index.html:9
msgid "Get started"
msgstr ""
msgstr "Comenzar"
#: app/templates/auth/login.html:15
msgid "Forgot password?"
msgstr ""
msgstr "¿Olvidaste tu contraseña?"
#: app/templates/auth/login.html:16 app/templates/auth/register.html:6
msgid "Create account"
msgstr ""
msgstr "Crear cuenta"
#: app/templates/auth/register.html:16
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
msgid "Set new password"
msgstr ""
msgstr "Establecer nueva contraseña"
#: app/templates/auth/reset_request.html:3
#: app/templates/auth/reset_request.html:6
msgid "Reset password"
msgstr ""
msgstr "Restablecer contraseña"
#: app/templates/errors/403.html:3
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/500.html:3
msgid "Back home"
msgstr ""
msgstr "Volver al inicio"
#: app/templates/errors/404.html:3
msgid "Not found."
msgstr ""
msgstr "No encontrado."
#: app/templates/errors/500.html:3
msgid "Something went wrong."
msgstr ""
msgstr "Algo salió mal."
#: app/templates/listings/browse.html:6
msgid "Filter"
msgstr ""
msgstr "Filtrar"
#: app/templates/listings/browse.html:9
msgid "Keyword"
msgstr ""
msgstr "Palabra clave"
#: app/templates/listings/browse.html:13
msgid "Category"
msgstr ""
msgstr "Categoría"
#: app/templates/listings/browse.html:15
msgid "All"
msgstr ""
msgstr "Todos"
#: app/templates/listings/browse.html:21
msgid "State"
msgstr ""
msgstr "Estado"
#: app/templates/listings/browse.html:24
msgid "Min $"
msgstr ""
msgstr "Mín $"
#: app/templates/listings/browse.html:26
msgid "Max $"
msgstr ""
msgstr "Máx $"
#: app/templates/listings/browse.html:30
msgid "Near ZIP"
msgstr ""
msgstr "Cerca del código postal"
#: app/templates/listings/browse.html:32
msgid "Radius (mi)"
msgstr ""
msgstr "Radio (mi)"
#: app/templates/listings/browse.html:35
msgid "Apply"
msgstr ""
msgstr "Aplicar"
#: app/templates/listings/browse.html:40
#, python-format
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
msgid "No listings found."
msgstr ""
msgstr "No se encontraron anuncios."
#: app/templates/listings/browse.html:46
msgid "No photo"
msgstr ""
msgstr "Sin foto"
#: app/templates/listings/browse.html:51
msgid "Featured"
msgstr ""
msgstr "Destacado"
#: app/templates/listings/browse.html:63
msgid "Prev"
msgstr ""
msgstr "Anterior"
#: app/templates/listings/browse.html:64
msgid "Next"
msgstr ""
msgstr "Siguiente"
#: app/templates/listings/detail.html:14
#, python-format
msgid "%(n)s views"
msgstr ""
msgstr "%(n)s vistas"
#: app/templates/listings/detail.html:41
msgid "Verified"
msgstr ""
msgstr "Verificado"
#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19
msgid "Edit"
msgstr ""
msgstr "Editar"
#: app/templates/listings/detail.html:47
msgid "Mark sold"
msgstr ""
msgstr "Marcar vendido"
#: app/templates/listings/detail.html:51
msgid "Delete this listing?"
msgstr ""
msgstr "¿Eliminar este anuncio?"
#: app/templates/listings/detail.html:52
msgid "Delete"
msgstr ""
msgstr "Eliminar"
#: app/templates/listings/detail.html:55
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
msgid "Edit listing"
msgstr ""
msgstr "Editar anuncio"
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "New listing"
msgstr ""
msgstr "Nuevo anuncio"
#: app/templates/listings/form.html:47
msgid "Current photos"
msgstr ""
msgstr "Fotos actuales"
#: app/templates/listings/mine.html:7
#, python-format
msgid "Active: %(a)s"
msgstr ""
msgstr "Activos: %(a)s"
#: app/templates/listings/mine.html:8
msgid "Post new"
msgstr ""
msgstr "Publicar nuevo"
#: app/templates/listings/mine.html:10
msgid "No listings yet."
msgstr ""
msgstr "Aún no hay anuncios."
#: app/templates/listings/mine.html:18
msgid "views"
msgstr ""
msgstr "vistas"
Binary file not shown.
+75 -75
View File
@@ -20,313 +20,313 @@ msgstr ""
#: app/blueprints/auth/routes.py:28
msgid "Verify your email"
msgstr ""
msgstr "Xác minh email của bạn"
#: app/blueprints/auth/routes.py:29
#, python-format
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
msgid "Captcha verification failed."
msgstr ""
msgstr "Xác minh Captcha thất bại."
#: app/blueprints/auth/routes.py:45
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
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
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
msgid "This account is suspended."
msgstr ""
msgstr "Tài khoản này đã bị tạm khóa."
#: app/blueprints/auth/routes.py:93
msgid "Signed out."
msgstr ""
msgstr "Đã đăng xuất."
#: app/blueprints/auth/routes.py:102
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
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
msgid "Reset your password"
msgstr ""
msgstr "Đặt lại mật khẩu của bạn"
#: app/blueprints/auth/routes.py:125
#, python-format
msgid "Reset link: %(link)s"
msgstr ""
msgstr "Liên kết đặt lại: %(link)s"
#: app/blueprints/auth/routes.py:127
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
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
msgid "Password updated. Sign in."
msgstr ""
msgstr "Đã cập nhật mật khẩu. Đăng nhập."
#: app/blueprints/listings/routes.py:72
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
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
#, python-format
msgid "%(f)s: %(m)s"
msgstr ""
msgstr "%(f)s: %(m)s"
#: app/blueprints/listings/routes.py:133
msgid "Listing published."
msgstr ""
msgstr "Đã đăng tin."
#: app/blueprints/listings/routes.py:176
msgid "Listing updated."
msgstr ""
msgstr "Đã cập nhật tin."
#: app/blueprints/listings/routes.py:194
msgid "Listing deleted."
msgstr ""
msgstr "Đã xóa tin."
#: app/blueprints/listings/routes.py:207
msgid "Marked as sold."
msgstr ""
msgstr "Đã đánh dấu là đã bán."
#: app/blueprints/listings/routes.py:236
msgid "Photo removed."
msgstr ""
msgstr "Đã xóa ảnh."
#: app/blueprints/listings/routes.py:258
#, python-format
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
#, python-format
msgid "Image skipped: %(m)s"
msgstr ""
msgstr "Đã bỏ qua ảnh: %(m)s"
#: app/templates/base.html:15
msgid "Browse"
msgstr ""
msgstr "Duyệt"
#: app/templates/base.html:17
msgid "Post"
msgstr ""
msgstr "Đăng tin"
#: app/templates/base.html:18 app/templates/listings/mine.html:2
#: app/templates/listings/mine.html:6
msgid "My listings"
msgstr ""
msgstr "Tin của tôi"
#: app/templates/base.html:20
msgid "Sign out"
msgstr ""
msgstr "Đăng xuất"
#: app/templates/auth/login.html:3 app/templates/auth/login.html:6
#: app/templates/base.html:22
msgid "Sign in"
msgstr ""
msgstr "Đăng nhập"
#: app/templates/auth/register.html:3 app/templates/base.html:23
msgid "Register"
msgstr ""
msgstr "Đăng ký"
#: app/templates/index.html:2
msgid "Classifieds — Home"
msgstr ""
msgstr "Rao vặt — Trang chủ"
#: app/templates/index.html:5
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
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
msgid "Browse listings"
msgstr ""
msgstr "Duyệt tin đăng"
#: app/templates/index.html:9
msgid "Get started"
msgstr ""
msgstr "Bắt đầu"
#: app/templates/auth/login.html:15
msgid "Forgot password?"
msgstr ""
msgstr "Quên mật khẩu?"
#: app/templates/auth/login.html:16 app/templates/auth/register.html:6
msgid "Create account"
msgstr ""
msgstr "Tạo tài khoản"
#: app/templates/auth/register.html:16
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
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:6
msgid "Reset password"
msgstr ""
msgstr "Đặt lại mật khẩu"
#: app/templates/errors/403.html:3
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/500.html:3
msgid "Back home"
msgstr ""
msgstr "Về trang chủ"
#: app/templates/errors/404.html:3
msgid "Not found."
msgstr ""
msgstr "Không tìm thấy."
#: app/templates/errors/500.html:3
msgid "Something went wrong."
msgstr ""
msgstr "Đã xảy ra lỗi."
#: app/templates/listings/browse.html:6
msgid "Filter"
msgstr ""
msgstr "Lọc"
#: app/templates/listings/browse.html:9
msgid "Keyword"
msgstr ""
msgstr "Từ khóa"
#: app/templates/listings/browse.html:13
msgid "Category"
msgstr ""
msgstr "Danh mục"
#: app/templates/listings/browse.html:15
msgid "All"
msgstr ""
msgstr "Tất cả"
#: app/templates/listings/browse.html:21
msgid "State"
msgstr ""
msgstr "Tiểu bang"
#: app/templates/listings/browse.html:24
msgid "Min $"
msgstr ""
msgstr "Giá tối thiểu $"
#: app/templates/listings/browse.html:26
msgid "Max $"
msgstr ""
msgstr "Giá tối đa $"
#: app/templates/listings/browse.html:30
msgid "Near ZIP"
msgstr ""
msgstr "Gần mã ZIP"
#: app/templates/listings/browse.html:32
msgid "Radius (mi)"
msgstr ""
msgstr "Bán kính (dặm)"
#: app/templates/listings/browse.html:35
msgid "Apply"
msgstr ""
msgstr "Áp dụng"
#: app/templates/listings/browse.html:40
#, python-format
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
msgid "No listings found."
msgstr ""
msgstr "Không tìm thấy tin đăng."
#: app/templates/listings/browse.html:46
msgid "No photo"
msgstr ""
msgstr "Không có ảnh"
#: app/templates/listings/browse.html:51
msgid "Featured"
msgstr ""
msgstr "Nổi bật"
#: app/templates/listings/browse.html:63
msgid "Prev"
msgstr ""
msgstr "Trước"
#: app/templates/listings/browse.html:64
msgid "Next"
msgstr ""
msgstr "Sau"
#: app/templates/listings/detail.html:14
#, python-format
msgid "%(n)s views"
msgstr ""
msgstr "%(n)s lượt xem"
#: app/templates/listings/detail.html:41
msgid "Verified"
msgstr ""
msgstr "Đã xác minh"
#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19
msgid "Edit"
msgstr ""
msgstr "Sửa"
#: app/templates/listings/detail.html:47
msgid "Mark sold"
msgstr ""
msgstr "Đánh dấu đã bán"
#: app/templates/listings/detail.html:51
msgid "Delete this listing?"
msgstr ""
msgstr "Xóa tin này?"
#: app/templates/listings/detail.html:52
msgid "Delete"
msgstr ""
msgstr "Xóa"
#: app/templates/listings/detail.html:55
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
msgid "Edit listing"
msgstr ""
msgstr "Sửa tin"
#: app/templates/listings/form.html:3 app/templates/listings/form.html:6
msgid "New listing"
msgstr ""
msgstr "Tin mới"
#: app/templates/listings/form.html:47
msgid "Current photos"
msgstr ""
msgstr "Ảnh hiện tại"
#: app/templates/listings/mine.html:7
#, python-format
msgid "Active: %(a)s"
msgstr ""
msgstr "Đang đăng: %(a)s"
#: app/templates/listings/mine.html:8
msgid "Post new"
msgstr ""
msgstr "Đăng tin mới"
#: app/templates/listings/mine.html:10
msgid "No listings yet."
msgstr ""
msgstr "Chưa có tin đăng."
#: app/templates/listings/mine.html:18
msgid "views"
msgstr ""
msgstr "lượt xem"