diff --git a/app/translations/es/LC_MESSAGES/messages.po b/app/translations/es/LC_MESSAGES/messages.po new file mode 100644 index 0000000..955e1b4 --- /dev/null +++ b/app/translations/es/LC_MESSAGES/messages.po @@ -0,0 +1,332 @@ +# Spanish translations for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-06-15 14:16-0400\n" +"PO-Revision-Date: 2026-06-15 14:16-0400\n" +"Last-Translator: FULL NAME \n" +"Language: es\n" +"Language-Team: es \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: app/blueprints/auth/routes.py:28 +msgid "Verify your email" +msgstr "" + +#: app/blueprints/auth/routes.py:29 +#, python-format +msgid "Confirm your account: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:40 +msgid "Captcha verification failed." +msgstr "" + +#: app/blueprints/auth/routes.py:45 +msgid "An account with that email already exists." +msgstr "" + +#: app/blueprints/auth/routes.py:59 +msgid "Account created. Check your email to verify." +msgstr "" + +#: app/blueprints/auth/routes.py:74 +msgid "Invalid email or password." +msgstr "" + +#: app/blueprints/auth/routes.py:77 +msgid "This account is suspended." +msgstr "" + +#: app/blueprints/auth/routes.py:93 +msgid "Signed out." +msgstr "" + +#: app/blueprints/auth/routes.py:102 +msgid "Verification link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:111 +msgid "Email verified. You're all set." +msgstr "" + +#: app/blueprints/auth/routes.py:124 +msgid "Reset your password" +msgstr "" + +#: app/blueprints/auth/routes.py:125 +#, python-format +msgid "Reset link: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:127 +msgid "If that email exists, a reset link has been sent." +msgstr "" + +#: app/blueprints/auth/routes.py:137 +msgid "Reset link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:146 +msgid "Password updated. Sign in." +msgstr "" + +#: app/blueprints/listings/routes.py:72 +msgid "ZIP not found; showing all results." +msgstr "" + +#: app/blueprints/listings/routes.py:103 +msgid "You've reached your plan's active-listing limit." +msgstr "" + +#: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171 +#, python-format +msgid "%(f)s: %(m)s" +msgstr "" + +#: app/blueprints/listings/routes.py:133 +msgid "Listing published." +msgstr "" + +#: app/blueprints/listings/routes.py:176 +msgid "Listing updated." +msgstr "" + +#: app/blueprints/listings/routes.py:194 +msgid "Listing deleted." +msgstr "" + +#: app/blueprints/listings/routes.py:207 +msgid "Marked as sold." +msgstr "" + +#: app/blueprints/listings/routes.py:236 +msgid "Photo removed." +msgstr "" + +#: app/blueprints/listings/routes.py:258 +#, python-format +msgid "Photo limit (%(n)s) reached for your plan." +msgstr "" + +#: app/blueprints/listings/routes.py:265 +#, python-format +msgid "Image skipped: %(m)s" +msgstr "" + +#: app/templates/base.html:15 +msgid "Browse" +msgstr "" + +#: app/templates/base.html:17 +msgid "Post" +msgstr "" + +#: app/templates/base.html:18 app/templates/listings/mine.html:2 +#: app/templates/listings/mine.html:6 +msgid "My listings" +msgstr "" + +#: app/templates/base.html:20 +msgid "Sign out" +msgstr "" + +#: app/templates/auth/login.html:3 app/templates/auth/login.html:6 +#: app/templates/base.html:22 +msgid "Sign in" +msgstr "" + +#: app/templates/auth/register.html:3 app/templates/base.html:23 +msgid "Register" +msgstr "" + +#: app/templates/index.html:2 +msgid "Classifieds — Home" +msgstr "" + +#: app/templates/index.html:5 +msgid "Find what you need. Post what you offer." +msgstr "" + +#: app/templates/index.html:6 +msgid "Buy, sell, request, hire, and connect across your community." +msgstr "" + +#: app/templates/index.html:7 app/templates/listings/browse.html:2 +msgid "Browse listings" +msgstr "" + +#: app/templates/index.html:9 +msgid "Get started" +msgstr "" + +#: app/templates/auth/login.html:15 +msgid "Forgot password?" +msgstr "" + +#: app/templates/auth/login.html:16 app/templates/auth/register.html:6 +msgid "Create account" +msgstr "" + +#: app/templates/auth/register.html:16 +msgid "Already have an account? Sign in" +msgstr "" + +#: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6 +msgid "Set new password" +msgstr "" + +#: app/templates/auth/reset_request.html:3 +#: app/templates/auth/reset_request.html:6 +msgid "Reset password" +msgstr "" + +#: app/templates/errors/403.html:3 +msgid "Forbidden — you do not have access." +msgstr "" + +#: app/templates/errors/403.html:3 app/templates/errors/404.html:3 +#: app/templates/errors/500.html:3 +msgid "Back home" +msgstr "" + +#: app/templates/errors/404.html:3 +msgid "Not found." +msgstr "" + +#: app/templates/errors/500.html:3 +msgid "Something went wrong." +msgstr "" + +#: app/templates/listings/browse.html:6 +msgid "Filter" +msgstr "" + +#: app/templates/listings/browse.html:9 +msgid "Keyword" +msgstr "" + +#: app/templates/listings/browse.html:13 +msgid "Category" +msgstr "" + +#: app/templates/listings/browse.html:15 +msgid "All" +msgstr "" + +#: app/templates/listings/browse.html:21 +msgid "State" +msgstr "" + +#: app/templates/listings/browse.html:24 +msgid "Min $" +msgstr "" + +#: app/templates/listings/browse.html:26 +msgid "Max $" +msgstr "" + +#: app/templates/listings/browse.html:30 +msgid "Near ZIP" +msgstr "" + +#: app/templates/listings/browse.html:32 +msgid "Radius (mi)" +msgstr "" + +#: app/templates/listings/browse.html:35 +msgid "Apply" +msgstr "" + +#: app/templates/listings/browse.html:40 +#, python-format +msgid "%(n)s results within %(r)s mi of %(z)s" +msgstr "" + +#: app/templates/listings/browse.html:41 +msgid "No listings found." +msgstr "" + +#: app/templates/listings/browse.html:46 +msgid "No photo" +msgstr "" + +#: app/templates/listings/browse.html:51 +msgid "Featured" +msgstr "" + +#: app/templates/listings/browse.html:63 +msgid "Prev" +msgstr "" + +#: app/templates/listings/browse.html:64 +msgid "Next" +msgstr "" + +#: app/templates/listings/detail.html:14 +#, python-format +msgid "%(n)s views" +msgstr "" + +#: app/templates/listings/detail.html:41 +msgid "Verified" +msgstr "" + +#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19 +msgid "Edit" +msgstr "" + +#: app/templates/listings/detail.html:47 +msgid "Mark sold" +msgstr "" + +#: app/templates/listings/detail.html:51 +msgid "Delete this listing?" +msgstr "" + +#: app/templates/listings/detail.html:52 +msgid "Delete" +msgstr "" + +#: app/templates/listings/detail.html:55 +msgid "Messaging arrives in Phase 3." +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "Edit listing" +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "New listing" +msgstr "" + +#: app/templates/listings/form.html:47 +msgid "Current photos" +msgstr "" + +#: app/templates/listings/mine.html:7 +#, python-format +msgid "Active: %(a)s" +msgstr "" + +#: app/templates/listings/mine.html:8 +msgid "Post new" +msgstr "" + +#: app/templates/listings/mine.html:10 +msgid "No listings yet." +msgstr "" + +#: app/templates/listings/mine.html:18 +msgid "views" +msgstr "" + diff --git a/app/translations/vi/LC_MESSAGES/messages.po b/app/translations/vi/LC_MESSAGES/messages.po new file mode 100644 index 0000000..126d035 --- /dev/null +++ b/app/translations/vi/LC_MESSAGES/messages.po @@ -0,0 +1,332 @@ +# Vietnamese translations for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-06-15 14:16-0400\n" +"PO-Revision-Date: 2026-06-15 14:16-0400\n" +"Last-Translator: FULL NAME \n" +"Language: vi\n" +"Language-Team: vi \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: app/blueprints/auth/routes.py:28 +msgid "Verify your email" +msgstr "" + +#: app/blueprints/auth/routes.py:29 +#, python-format +msgid "Confirm your account: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:40 +msgid "Captcha verification failed." +msgstr "" + +#: app/blueprints/auth/routes.py:45 +msgid "An account with that email already exists." +msgstr "" + +#: app/blueprints/auth/routes.py:59 +msgid "Account created. Check your email to verify." +msgstr "" + +#: app/blueprints/auth/routes.py:74 +msgid "Invalid email or password." +msgstr "" + +#: app/blueprints/auth/routes.py:77 +msgid "This account is suspended." +msgstr "" + +#: app/blueprints/auth/routes.py:93 +msgid "Signed out." +msgstr "" + +#: app/blueprints/auth/routes.py:102 +msgid "Verification link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:111 +msgid "Email verified. You're all set." +msgstr "" + +#: app/blueprints/auth/routes.py:124 +msgid "Reset your password" +msgstr "" + +#: app/blueprints/auth/routes.py:125 +#, python-format +msgid "Reset link: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:127 +msgid "If that email exists, a reset link has been sent." +msgstr "" + +#: app/blueprints/auth/routes.py:137 +msgid "Reset link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:146 +msgid "Password updated. Sign in." +msgstr "" + +#: app/blueprints/listings/routes.py:72 +msgid "ZIP not found; showing all results." +msgstr "" + +#: app/blueprints/listings/routes.py:103 +msgid "You've reached your plan's active-listing limit." +msgstr "" + +#: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171 +#, python-format +msgid "%(f)s: %(m)s" +msgstr "" + +#: app/blueprints/listings/routes.py:133 +msgid "Listing published." +msgstr "" + +#: app/blueprints/listings/routes.py:176 +msgid "Listing updated." +msgstr "" + +#: app/blueprints/listings/routes.py:194 +msgid "Listing deleted." +msgstr "" + +#: app/blueprints/listings/routes.py:207 +msgid "Marked as sold." +msgstr "" + +#: app/blueprints/listings/routes.py:236 +msgid "Photo removed." +msgstr "" + +#: app/blueprints/listings/routes.py:258 +#, python-format +msgid "Photo limit (%(n)s) reached for your plan." +msgstr "" + +#: app/blueprints/listings/routes.py:265 +#, python-format +msgid "Image skipped: %(m)s" +msgstr "" + +#: app/templates/base.html:15 +msgid "Browse" +msgstr "" + +#: app/templates/base.html:17 +msgid "Post" +msgstr "" + +#: app/templates/base.html:18 app/templates/listings/mine.html:2 +#: app/templates/listings/mine.html:6 +msgid "My listings" +msgstr "" + +#: app/templates/base.html:20 +msgid "Sign out" +msgstr "" + +#: app/templates/auth/login.html:3 app/templates/auth/login.html:6 +#: app/templates/base.html:22 +msgid "Sign in" +msgstr "" + +#: app/templates/auth/register.html:3 app/templates/base.html:23 +msgid "Register" +msgstr "" + +#: app/templates/index.html:2 +msgid "Classifieds — Home" +msgstr "" + +#: app/templates/index.html:5 +msgid "Find what you need. Post what you offer." +msgstr "" + +#: app/templates/index.html:6 +msgid "Buy, sell, request, hire, and connect across your community." +msgstr "" + +#: app/templates/index.html:7 app/templates/listings/browse.html:2 +msgid "Browse listings" +msgstr "" + +#: app/templates/index.html:9 +msgid "Get started" +msgstr "" + +#: app/templates/auth/login.html:15 +msgid "Forgot password?" +msgstr "" + +#: app/templates/auth/login.html:16 app/templates/auth/register.html:6 +msgid "Create account" +msgstr "" + +#: app/templates/auth/register.html:16 +msgid "Already have an account? Sign in" +msgstr "" + +#: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6 +msgid "Set new password" +msgstr "" + +#: app/templates/auth/reset_request.html:3 +#: app/templates/auth/reset_request.html:6 +msgid "Reset password" +msgstr "" + +#: app/templates/errors/403.html:3 +msgid "Forbidden — you do not have access." +msgstr "" + +#: app/templates/errors/403.html:3 app/templates/errors/404.html:3 +#: app/templates/errors/500.html:3 +msgid "Back home" +msgstr "" + +#: app/templates/errors/404.html:3 +msgid "Not found." +msgstr "" + +#: app/templates/errors/500.html:3 +msgid "Something went wrong." +msgstr "" + +#: app/templates/listings/browse.html:6 +msgid "Filter" +msgstr "" + +#: app/templates/listings/browse.html:9 +msgid "Keyword" +msgstr "" + +#: app/templates/listings/browse.html:13 +msgid "Category" +msgstr "" + +#: app/templates/listings/browse.html:15 +msgid "All" +msgstr "" + +#: app/templates/listings/browse.html:21 +msgid "State" +msgstr "" + +#: app/templates/listings/browse.html:24 +msgid "Min $" +msgstr "" + +#: app/templates/listings/browse.html:26 +msgid "Max $" +msgstr "" + +#: app/templates/listings/browse.html:30 +msgid "Near ZIP" +msgstr "" + +#: app/templates/listings/browse.html:32 +msgid "Radius (mi)" +msgstr "" + +#: app/templates/listings/browse.html:35 +msgid "Apply" +msgstr "" + +#: app/templates/listings/browse.html:40 +#, python-format +msgid "%(n)s results within %(r)s mi of %(z)s" +msgstr "" + +#: app/templates/listings/browse.html:41 +msgid "No listings found." +msgstr "" + +#: app/templates/listings/browse.html:46 +msgid "No photo" +msgstr "" + +#: app/templates/listings/browse.html:51 +msgid "Featured" +msgstr "" + +#: app/templates/listings/browse.html:63 +msgid "Prev" +msgstr "" + +#: app/templates/listings/browse.html:64 +msgid "Next" +msgstr "" + +#: app/templates/listings/detail.html:14 +#, python-format +msgid "%(n)s views" +msgstr "" + +#: app/templates/listings/detail.html:41 +msgid "Verified" +msgstr "" + +#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19 +msgid "Edit" +msgstr "" + +#: app/templates/listings/detail.html:47 +msgid "Mark sold" +msgstr "" + +#: app/templates/listings/detail.html:51 +msgid "Delete this listing?" +msgstr "" + +#: app/templates/listings/detail.html:52 +msgid "Delete" +msgstr "" + +#: app/templates/listings/detail.html:55 +msgid "Messaging arrives in Phase 3." +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "Edit listing" +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "New listing" +msgstr "" + +#: app/templates/listings/form.html:47 +msgid "Current photos" +msgstr "" + +#: app/templates/listings/mine.html:7 +#, python-format +msgid "Active: %(a)s" +msgstr "" + +#: app/templates/listings/mine.html:8 +msgid "Post new" +msgstr "" + +#: app/templates/listings/mine.html:10 +msgid "No listings yet." +msgstr "" + +#: app/templates/listings/mine.html:18 +msgid "views" +msgstr "" + diff --git a/messages.pot b/messages.pot new file mode 100644 index 0000000..de6de3e --- /dev/null +++ b/messages.pot @@ -0,0 +1,331 @@ +# Translations template for PROJECT. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2026-06-15 14:16-0400\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: app/blueprints/auth/routes.py:28 +msgid "Verify your email" +msgstr "" + +#: app/blueprints/auth/routes.py:29 +#, python-format +msgid "Confirm your account: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:40 +msgid "Captcha verification failed." +msgstr "" + +#: app/blueprints/auth/routes.py:45 +msgid "An account with that email already exists." +msgstr "" + +#: app/blueprints/auth/routes.py:59 +msgid "Account created. Check your email to verify." +msgstr "" + +#: app/blueprints/auth/routes.py:74 +msgid "Invalid email or password." +msgstr "" + +#: app/blueprints/auth/routes.py:77 +msgid "This account is suspended." +msgstr "" + +#: app/blueprints/auth/routes.py:93 +msgid "Signed out." +msgstr "" + +#: app/blueprints/auth/routes.py:102 +msgid "Verification link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:111 +msgid "Email verified. You're all set." +msgstr "" + +#: app/blueprints/auth/routes.py:124 +msgid "Reset your password" +msgstr "" + +#: app/blueprints/auth/routes.py:125 +#, python-format +msgid "Reset link: %(link)s" +msgstr "" + +#: app/blueprints/auth/routes.py:127 +msgid "If that email exists, a reset link has been sent." +msgstr "" + +#: app/blueprints/auth/routes.py:137 +msgid "Reset link is invalid or expired." +msgstr "" + +#: app/blueprints/auth/routes.py:146 +msgid "Password updated. Sign in." +msgstr "" + +#: app/blueprints/listings/routes.py:72 +msgid "ZIP not found; showing all results." +msgstr "" + +#: app/blueprints/listings/routes.py:103 +msgid "You've reached your plan's active-listing limit." +msgstr "" + +#: app/blueprints/listings/routes.py:125 app/blueprints/listings/routes.py:171 +#, python-format +msgid "%(f)s: %(m)s" +msgstr "" + +#: app/blueprints/listings/routes.py:133 +msgid "Listing published." +msgstr "" + +#: app/blueprints/listings/routes.py:176 +msgid "Listing updated." +msgstr "" + +#: app/blueprints/listings/routes.py:194 +msgid "Listing deleted." +msgstr "" + +#: app/blueprints/listings/routes.py:207 +msgid "Marked as sold." +msgstr "" + +#: app/blueprints/listings/routes.py:236 +msgid "Photo removed." +msgstr "" + +#: app/blueprints/listings/routes.py:258 +#, python-format +msgid "Photo limit (%(n)s) reached for your plan." +msgstr "" + +#: app/blueprints/listings/routes.py:265 +#, python-format +msgid "Image skipped: %(m)s" +msgstr "" + +#: app/templates/base.html:15 +msgid "Browse" +msgstr "" + +#: app/templates/base.html:17 +msgid "Post" +msgstr "" + +#: app/templates/base.html:18 app/templates/listings/mine.html:2 +#: app/templates/listings/mine.html:6 +msgid "My listings" +msgstr "" + +#: app/templates/base.html:20 +msgid "Sign out" +msgstr "" + +#: app/templates/auth/login.html:3 app/templates/auth/login.html:6 +#: app/templates/base.html:22 +msgid "Sign in" +msgstr "" + +#: app/templates/auth/register.html:3 app/templates/base.html:23 +msgid "Register" +msgstr "" + +#: app/templates/index.html:2 +msgid "Classifieds — Home" +msgstr "" + +#: app/templates/index.html:5 +msgid "Find what you need. Post what you offer." +msgstr "" + +#: app/templates/index.html:6 +msgid "Buy, sell, request, hire, and connect across your community." +msgstr "" + +#: app/templates/index.html:7 app/templates/listings/browse.html:2 +msgid "Browse listings" +msgstr "" + +#: app/templates/index.html:9 +msgid "Get started" +msgstr "" + +#: app/templates/auth/login.html:15 +msgid "Forgot password?" +msgstr "" + +#: app/templates/auth/login.html:16 app/templates/auth/register.html:6 +msgid "Create account" +msgstr "" + +#: app/templates/auth/register.html:16 +msgid "Already have an account? Sign in" +msgstr "" + +#: app/templates/auth/reset.html:3 app/templates/auth/reset.html:6 +msgid "Set new password" +msgstr "" + +#: app/templates/auth/reset_request.html:3 +#: app/templates/auth/reset_request.html:6 +msgid "Reset password" +msgstr "" + +#: app/templates/errors/403.html:3 +msgid "Forbidden — you do not have access." +msgstr "" + +#: app/templates/errors/403.html:3 app/templates/errors/404.html:3 +#: app/templates/errors/500.html:3 +msgid "Back home" +msgstr "" + +#: app/templates/errors/404.html:3 +msgid "Not found." +msgstr "" + +#: app/templates/errors/500.html:3 +msgid "Something went wrong." +msgstr "" + +#: app/templates/listings/browse.html:6 +msgid "Filter" +msgstr "" + +#: app/templates/listings/browse.html:9 +msgid "Keyword" +msgstr "" + +#: app/templates/listings/browse.html:13 +msgid "Category" +msgstr "" + +#: app/templates/listings/browse.html:15 +msgid "All" +msgstr "" + +#: app/templates/listings/browse.html:21 +msgid "State" +msgstr "" + +#: app/templates/listings/browse.html:24 +msgid "Min $" +msgstr "" + +#: app/templates/listings/browse.html:26 +msgid "Max $" +msgstr "" + +#: app/templates/listings/browse.html:30 +msgid "Near ZIP" +msgstr "" + +#: app/templates/listings/browse.html:32 +msgid "Radius (mi)" +msgstr "" + +#: app/templates/listings/browse.html:35 +msgid "Apply" +msgstr "" + +#: app/templates/listings/browse.html:40 +#, python-format +msgid "%(n)s results within %(r)s mi of %(z)s" +msgstr "" + +#: app/templates/listings/browse.html:41 +msgid "No listings found." +msgstr "" + +#: app/templates/listings/browse.html:46 +msgid "No photo" +msgstr "" + +#: app/templates/listings/browse.html:51 +msgid "Featured" +msgstr "" + +#: app/templates/listings/browse.html:63 +msgid "Prev" +msgstr "" + +#: app/templates/listings/browse.html:64 +msgid "Next" +msgstr "" + +#: app/templates/listings/detail.html:14 +#, python-format +msgid "%(n)s views" +msgstr "" + +#: app/templates/listings/detail.html:41 +msgid "Verified" +msgstr "" + +#: app/templates/listings/detail.html:44 app/templates/listings/mine.html:19 +msgid "Edit" +msgstr "" + +#: app/templates/listings/detail.html:47 +msgid "Mark sold" +msgstr "" + +#: app/templates/listings/detail.html:51 +msgid "Delete this listing?" +msgstr "" + +#: app/templates/listings/detail.html:52 +msgid "Delete" +msgstr "" + +#: app/templates/listings/detail.html:55 +msgid "Messaging arrives in Phase 3." +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "Edit listing" +msgstr "" + +#: app/templates/listings/form.html:3 app/templates/listings/form.html:6 +msgid "New listing" +msgstr "" + +#: app/templates/listings/form.html:47 +msgid "Current photos" +msgstr "" + +#: app/templates/listings/mine.html:7 +#, python-format +msgid "Active: %(a)s" +msgstr "" + +#: app/templates/listings/mine.html:8 +msgid "Post new" +msgstr "" + +#: app/templates/listings/mine.html:10 +msgid "No listings yet." +msgstr "" + +#: app/templates/listings/mine.html:18 +msgid "views" +msgstr "" + diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/92815e03f343_phase1_2_schema.py b/migrations/versions/92815e03f343_phase1_2_schema.py new file mode 100644 index 0000000..0d932db --- /dev/null +++ b/migrations/versions/92815e03f343_phase1_2_schema.py @@ -0,0 +1,224 @@ +"""phase1+2 schema + +Revision ID: 92815e03f343 +Revises: +Create Date: 2026-06-15 11:55:07.300934 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '92815e03f343' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('categories', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('slug', sa.String(length=60), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('parent_id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), nullable=True), + sa.Column('field_schema', sa.JSON(), nullable=False), + sa.Column('icon', sa.String(length=40), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.Column('sponsor_id', sa.BigInteger(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['parent_id'], ['categories.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('categories', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_categories_parent_id'), ['parent_id'], unique=False) + batch_op.create_index(batch_op.f('ix_categories_slug'), ['slug'], unique=True) + + op.create_table('metros', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('slug', sa.String(length=80), nullable=False), + sa.Column('name', sa.String(length=120), nullable=False), + sa.Column('state', sa.String(length=2), nullable=True), + sa.Column('center_lat', sa.Float(), nullable=True), + sa.Column('center_lng', sa.Float(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('metros', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_metros_slug'), ['slug'], unique=True) + + op.create_table('plans', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('slug', sa.String(length=40), nullable=False), + sa.Column('name', sa.String(length=80), nullable=False), + sa.Column('price_monthly_cents', sa.Integer(), nullable=False), + sa.Column('stripe_price_id', sa.String(length=80), nullable=True), + sa.Column('config', sa.JSON(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('slug') + ) + op.create_table('zip_geo', + sa.Column('zip', sa.String(length=12), nullable=False), + sa.Column('city', sa.String(length=80), nullable=False), + sa.Column('state', sa.String(length=2), nullable=False), + sa.Column('lat', sa.Float(), nullable=False), + sa.Column('lng', sa.Float(), nullable=False), + sa.Column('metro', sa.String(length=80), nullable=True), + sa.PrimaryKeyConstraint('zip') + ) + with op.batch_alter_table('zip_geo', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_zip_geo_metro'), ['metro'], unique=False) + batch_op.create_index(batch_op.f('ix_zip_geo_state'), ['state'], unique=False) + + op.create_table('users', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('password_hash', sa.String(length=255), nullable=False), + sa.Column('display_name', sa.String(length=80), nullable=False), + sa.Column('role', sa.Enum('free', 'subscriber', 'moderator', 'admin', name='role'), nullable=False), + sa.Column('status', sa.Enum('active', 'suspended', 'banned', name='userstatus'), nullable=False), + sa.Column('locale', sa.String(length=5), nullable=False), + sa.Column('tier_id', sa.BigInteger(), nullable=True), + sa.Column('trust_score', sa.Integer(), nullable=False), + sa.Column('trust_tier', sa.Enum('new', 'basic', 'trusted', 'verified', name='trusttier'), nullable=False), + sa.Column('verified', sa.Boolean(), nullable=False), + sa.Column('email_verified', sa.Boolean(), nullable=False), + sa.Column('last_login_at', sa.DateTime(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['tier_id'], ['plans.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_users_email'), ['email'], unique=True) + + op.create_table('listings', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('user_id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), nullable=False), + sa.Column('category_id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), nullable=False), + sa.Column('title', sa.String(length=140), nullable=False), + sa.Column('title_norm', sa.String(length=140), nullable=False), + sa.Column('body', sa.Text(), nullable=False), + sa.Column('lang', sa.Enum('en', 'vi', 'es', name='lang'), nullable=False), + sa.Column('price_cents', sa.Integer(), nullable=True), + sa.Column('zip', sa.String(length=12), nullable=True), + sa.Column('city', sa.String(length=80), nullable=True), + sa.Column('state', sa.String(length=2), nullable=True), + sa.Column('lat', sa.Float(), nullable=True), + sa.Column('lng', sa.Float(), nullable=True), + sa.Column('attributes', sa.JSON(), nullable=False), + sa.Column('attr_condition', sa.String(length=40), nullable=True), + sa.Column('attr_job_type', sa.String(length=40), nullable=True), + sa.Column('attr_salary_min', sa.Integer(), nullable=True), + sa.Column('attr_salary_max', sa.Integer(), nullable=True), + sa.Column('status', sa.Enum('active', 'flagged', 'sold', 'expired', 'removed', name='listingstatus'), nullable=False), + sa.Column('is_featured', sa.Boolean(), nullable=False), + sa.Column('bump_at', sa.DateTime(), nullable=True), + sa.Column('flag_count', sa.Integer(), nullable=False), + sa.Column('view_count', sa.Integer(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['category_id'], ['categories.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('listings', schema=None) as batch_op: + batch_op.create_index('ix_listing_browse', ['status', 'expires_at'], unique=False) + batch_op.create_index('ix_listing_sort', ['is_featured', 'bump_at'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_attr_condition'), ['attr_condition'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_attr_job_type'), ['attr_job_type'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_attr_salary_max'), ['attr_salary_max'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_attr_salary_min'), ['attr_salary_min'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_category_id'), ['category_id'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_expires_at'), ['expires_at'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_price_cents'), ['price_cents'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_state'), ['state'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_status'), ['status'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_title_norm'), ['title_norm'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_user_id'), ['user_id'], unique=False) + batch_op.create_index(batch_op.f('ix_listings_zip'), ['zip'], unique=False) + + op.create_table('trust_events', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('user_id', sa.BigInteger(), nullable=False), + sa.Column('type', sa.Enum('account_age', 'listing_survived', 'flag_received', 'verified_email', 'payment', name='trusteventtype'), nullable=False), + sa.Column('delta', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('trust_events', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_trust_events_user_id'), ['user_id'], unique=False) + + op.create_table('listing_images', + sa.Column('id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), autoincrement=True, nullable=False), + sa.Column('listing_id', sa.BigInteger().with_variant(sa.Integer(), 'sqlite'), nullable=False), + sa.Column('path', sa.String(length=255), nullable=False), + sa.Column('thumb_path', sa.String(length=255), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.Column('width', sa.Integer(), nullable=True), + sa.Column('height', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['listing_id'], ['listings.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('listing_images', schema=None) as batch_op: + batch_op.create_index(batch_op.f('ix_listing_images_listing_id'), ['listing_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('listing_images', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_listing_images_listing_id')) + + op.drop_table('listing_images') + with op.batch_alter_table('trust_events', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_trust_events_user_id')) + + op.drop_table('trust_events') + with op.batch_alter_table('listings', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_listings_zip')) + batch_op.drop_index(batch_op.f('ix_listings_user_id')) + batch_op.drop_index(batch_op.f('ix_listings_title_norm')) + batch_op.drop_index(batch_op.f('ix_listings_status')) + batch_op.drop_index(batch_op.f('ix_listings_state')) + batch_op.drop_index(batch_op.f('ix_listings_price_cents')) + batch_op.drop_index(batch_op.f('ix_listings_expires_at')) + batch_op.drop_index(batch_op.f('ix_listings_category_id')) + batch_op.drop_index(batch_op.f('ix_listings_attr_salary_min')) + batch_op.drop_index(batch_op.f('ix_listings_attr_salary_max')) + batch_op.drop_index(batch_op.f('ix_listings_attr_job_type')) + batch_op.drop_index(batch_op.f('ix_listings_attr_condition')) + batch_op.drop_index('ix_listing_sort') + batch_op.drop_index('ix_listing_browse') + + op.drop_table('listings') + with op.batch_alter_table('users', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_users_email')) + + op.drop_table('users') + with op.batch_alter_table('zip_geo', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_zip_geo_state')) + batch_op.drop_index(batch_op.f('ix_zip_geo_metro')) + + op.drop_table('zip_geo') + op.drop_table('plans') + with op.batch_alter_table('metros', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_metros_slug')) + + op.drop_table('metros') + with op.batch_alter_table('categories', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('ix_categories_slug')) + batch_op.drop_index(batch_op.f('ix_categories_parent_id')) + + op.drop_table('categories') + # ### end Alembic commands ###