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 0000000..8fef543 Binary files /dev/null and b/app/translations/es/LC_MESSAGES/messages.mo differ diff --git a/app/translations/es/LC_MESSAGES/messages.po b/app/translations/es/LC_MESSAGES/messages.po index 955e1b4..521a613 100644 --- a/app/translations/es/LC_MESSAGES/messages.po +++ b/app/translations/es/LC_MESSAGES/messages.po @@ -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" diff --git a/app/translations/vi/LC_MESSAGES/messages.mo b/app/translations/vi/LC_MESSAGES/messages.mo new file mode 100644 index 0000000..f1fa179 Binary files /dev/null and b/app/translations/vi/LC_MESSAGES/messages.mo differ diff --git a/app/translations/vi/LC_MESSAGES/messages.po b/app/translations/vi/LC_MESSAGES/messages.po index 126d035..2245021 100644 --- a/app/translations/vi/LC_MESSAGES/messages.po +++ b/app/translations/vi/LC_MESSAGES/messages.po @@ -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"