From 5048f865f2b4b6b38ed0920699c62f68c85dbb46 Mon Sep 17 00:00:00 2001 From: Warren Chen Date: Thu, 12 Mar 2026 13:42:06 +0900 Subject: [PATCH] Add internationalization support and update translations - Updated Wagtail hooks to use gettext for translatable strings in the NewsletterCampaignViewSet and related help texts. - Added LOCALE_PATHS to settings for loading translation files. - Updated base.html to include favicon links. - Translated various strings in the wagtailsettings edit template to support internationalization. - Created and populated zh_Hant translation files for Django messages. - Added a favicon.ico file to the static directory. --- innovedus_cms/base/models.py | 264 ++++++----- innovedus_cms/base/wagtail_hooks.py | 13 +- .../locale/zh_Hant/LC_MESSAGES/django.mo | Bin 0 -> 8770 bytes .../locale/zh_Hant/LC_MESSAGES/django.po | 432 ++++++++++++++++++ innovedus_cms/mysite/settings/base.py | 4 + innovedus_cms/mysite/static/favicon.ico | Bin 0 -> 728 bytes innovedus_cms/mysite/templates/base.html | 2 + .../templates/wagtailsettings/edit.html | 8 +- 8 files changed, 599 insertions(+), 124 deletions(-) create mode 100644 innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.mo create mode 100644 innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.po create mode 100644 innovedus_cms/mysite/static/favicon.ico diff --git a/innovedus_cms/base/models.py b/innovedus_cms/base/models.py index ac7da36..e2ff281 100644 --- a/innovedus_cms/base/models.py +++ b/innovedus_cms/base/models.py @@ -1,5 +1,6 @@ from django import forms as django_forms from django.db import models +from django.utils.translation import gettext_lazy as _ from wagtail.admin.panels import ( FieldPanel, MultiFieldPanel, @@ -39,7 +40,8 @@ class HeaderSettings(BaseGenericSetting): blank=True, on_delete=models.SET_NULL, related_name="+", - help_text="深色底用(亮色 logo)", + help_text=_("Use on dark background (light logo)."), + verbose_name=_("Light Logo"), ) logo_dark = models.ForeignKey( "wagtailimages.Image", @@ -47,15 +49,16 @@ class HeaderSettings(BaseGenericSetting): blank=True, on_delete=models.SET_NULL, related_name="+", - help_text="淺色底用(深色 logo)", + help_text=_("Use on light background (dark logo)."), + verbose_name=_("Dark Logo"), ) - site_name = models.CharField(max_length=255, blank=True) + site_name = models.CharField(max_length=255, blank=True, verbose_name=_("Site Name")) extra_links = StreamField([ ("link", blocks.StructBlock([ - ("label", blocks.CharBlock()), - ("url", blocks.URLBlock()) + ("label", blocks.CharBlock(label=_("Label"))), + ("url", blocks.URLBlock(label=_("URL"))) ])) - ], use_json_field=True, blank=True, null=True) + ], use_json_field=True, blank=True, null=True, verbose_name=_("Extra Links")) panels = [ MultiFieldPanel( @@ -65,70 +68,74 @@ class HeaderSettings(BaseGenericSetting): FieldPanel("site_name"), FieldPanel("extra_links"), ], - heading="Header Settings", + heading=_("Header Settings"), ), ] class Meta: - verbose_name = "Header Settings" + verbose_name = _("Header Settings") @register_setting class NavigationSettings(BaseGenericSetting): footer_links = StreamField([ ("section", blocks.StructBlock([ - ("title", blocks.CharBlock(required=False)), + ("title", blocks.CharBlock(required=False, label=_("Section Title"))), ("links", blocks.ListBlock(blocks.StructBlock([ - ("label", blocks.CharBlock()), - ("url", blocks.URLBlock()) + ("label", blocks.CharBlock(label=_("Label"))), + ("url", blocks.URLBlock(label=_("URL"))) ]))), ])) - ], use_json_field=True, blank=True, null=True) + ], use_json_field=True, blank=True, null=True, verbose_name=_("Footer Links")) panels = [ FieldPanel("footer_links"), ] class Meta: - verbose_name = "Footer Navigation" + verbose_name = _("Footer Navigation") class SocialLinkBlock(blocks.StructBlock): SOCIAL_MEDIA_CHOICES = [ - ("facebook", "Facebook"), - ("twitter", "Twitter"), - ("instagram", "Instagram"), - ("threads", "Threads"), - ("linkedin", "LinkedIn"), - ("youtube", "YouTube"), + ("facebook", _("Facebook")), + ("twitter", _("Twitter")), + ("instagram", _("Instagram")), + ("threads", _("Threads")), + ("linkedin", _("LinkedIn")), + ("youtube", _("YouTube")), ] - platform = blocks.ChoiceBlock(choices=SOCIAL_MEDIA_CHOICES) - url = blocks.URLBlock() + platform = blocks.ChoiceBlock(choices=SOCIAL_MEDIA_CHOICES, label=_("Platform")) + url = blocks.URLBlock(label=_("URL")) class Meta: icon = "link" - label = "Social Link" + label = _("Social Link") @register_setting class SocialMediaSettings(BaseGenericSetting): links = StreamField([ ("link", SocialLinkBlock()), - ], use_json_field=True) + ], use_json_field=True, verbose_name=_("Social Links")) panels = [FieldPanel("links")] + class Meta: + verbose_name = _("Social Media Settings") + @register_setting class MailSmtpSettings(BaseGenericSetting): - smtp_relay_host = models.CharField(max_length=255, blank=True) - smtp_relay_port = models.PositiveIntegerField(default=587) - smtp_use_tls = models.BooleanField(default=True) + smtp_relay_host = models.CharField(max_length=255, blank=True, verbose_name=_("SMTP Relay Host")) + smtp_relay_port = models.PositiveIntegerField(default=587, verbose_name=_("SMTP Relay Port")) + smtp_use_tls = models.BooleanField(default=True, verbose_name=_("Use TLS")) smtp_use_ssl = models.BooleanField( default=False, - help_text="465 常用 SSL(Implicit TLS);587 常用 STARTTLS(TLS)。", + help_text=_("Port 465 usually uses SSL (Implicit TLS); port 587 usually uses STARTTLS (TLS)."), + verbose_name=_("Use SSL"), ) - smtp_timeout_seconds = models.PositiveIntegerField(default=15) - smtp_username = models.CharField(max_length=255, blank=True) - smtp_password = models.TextField(blank=True) + smtp_timeout_seconds = models.PositiveIntegerField(default=15, verbose_name=_("SMTP Timeout Seconds")) + smtp_username = models.CharField(max_length=255, blank=True, verbose_name=_("SMTP Username")) + smtp_password = models.TextField(blank=True, verbose_name=_("SMTP Password")) panels = [ MultiFieldPanel( @@ -144,12 +151,12 @@ class MailSmtpSettings(BaseGenericSetting): widget=django_forms.PasswordInput(render_value=False), ), ], - heading="SMTP Settings", + heading=_("SMTP Settings"), ), ] class Meta: - verbose_name = "SMTP Settings" + verbose_name = _("SMTP Settings") def save(self, *args, **kwargs): if self.smtp_use_tls and self.smtp_use_ssl: @@ -166,83 +173,95 @@ class MailSmtpSettings(BaseGenericSetting): @register_setting class NewsletterSystemSettings(BaseGenericSetting): - member_center_base_url = models.URLField(blank=True) + member_center_base_url = models.URLField(blank=True, verbose_name=_("Member Center Base URL")) member_center_subscribe_path = models.CharField( max_length=255, blank=True, default="/newsletter/subscribe", + verbose_name=_("Subscribe Path"), ) member_center_confirm_path = models.CharField( max_length=255, blank=True, default="/newsletter/confirm", + verbose_name=_("Confirm Path"), ) member_center_unsubscribe_token_path = models.CharField( max_length=255, blank=True, default="/newsletter/unsubscribe-token", + verbose_name=_("Unsubscribe Token Path"), ) member_center_unsubscribe_path = models.CharField( max_length=255, blank=True, default="/newsletter/unsubscribe", + verbose_name=_("Unsubscribe Path"), ) member_center_subscriptions_path = models.CharField( max_length=255, blank=True, default="/newsletter/subscriptions", + verbose_name=_("Subscriptions Path"), ) member_center_oauth_token_path = models.CharField( max_length=255, blank=True, default="/oauth/token", + verbose_name=_("OAuth Token Path"), ) - member_center_oauth_client_id = models.CharField(max_length=255, blank=True) - member_center_oauth_client_secret = models.TextField(blank=True) + member_center_oauth_client_id = models.CharField(max_length=255, blank=True, verbose_name=_("OAuth Client ID")) + member_center_oauth_client_secret = models.TextField(blank=True, verbose_name=_("OAuth Client Secret")) member_center_oauth_scope = models.CharField( max_length=255, blank=True, default="newsletter:list.read", + verbose_name=_("OAuth Scope"), ) - member_center_oauth_audience = models.CharField(max_length=255, blank=True) + member_center_oauth_audience = models.CharField(max_length=255, blank=True, verbose_name=_("OAuth Audience")) member_center_one_click_unsubscribe_path = models.CharField( max_length=255, blank=True, default="/api/subscriptions/unsubscribe", + verbose_name=_("One-Click Unsubscribe Path"), ) - member_center_tenant_id = models.CharField(max_length=128, blank=True) - member_center_list_id = models.CharField(max_length=128, blank=True) - member_center_timeout_seconds = models.PositiveIntegerField(default=10) + member_center_tenant_id = models.CharField(max_length=128, blank=True, verbose_name=_("Tenant ID")) + member_center_list_id = models.CharField(max_length=128, blank=True, verbose_name=_("List ID")) + member_center_timeout_seconds = models.PositiveIntegerField(default=10, verbose_name=_("Member Center Timeout Seconds")) - send_engine_base_url = models.URLField(blank=True) + send_engine_base_url = models.URLField(blank=True, verbose_name=_("Send Engine Base URL")) send_engine_send_jobs_path = models.CharField( max_length=255, blank=True, default="/api/send-jobs", + verbose_name=_("Send Jobs Path"), ) - send_engine_oauth_scope = models.CharField(max_length=255, blank=True) - send_engine_timeout_seconds = models.PositiveIntegerField(default=10) - send_engine_retry_interval_seconds = models.PositiveIntegerField(default=300) - send_engine_retry_max_attempts = models.PositiveIntegerField(default=3) + send_engine_oauth_scope = models.CharField(max_length=255, blank=True, verbose_name=_("Send Engine OAuth Scope")) + send_engine_timeout_seconds = models.PositiveIntegerField(default=10, verbose_name=_("Send Engine Timeout Seconds")) + send_engine_retry_interval_seconds = models.PositiveIntegerField(default=300, verbose_name=_("Retry Interval Seconds")) + send_engine_retry_max_attempts = models.PositiveIntegerField(default=3, verbose_name=_("Retry Max Attempts")) - sender_name = models.CharField(max_length=255, blank=True) - sender_email = models.EmailField(blank=True) - reply_to_email = models.EmailField(blank=True) - default_charset = models.CharField(max_length=50, default="utf-8") + sender_name = models.CharField(max_length=255, blank=True, verbose_name=_("Sender Name")) + sender_email = models.EmailField(blank=True, verbose_name=_("Sender Email")) + reply_to_email = models.EmailField(blank=True, verbose_name=_("Reply-To Email")) + default_charset = models.CharField(max_length=50, default="utf-8", verbose_name=_("Default Charset")) one_click_endpoint_path = models.CharField( max_length=255, blank=True, default="/u/unsubscribe", + verbose_name=_("One-Click Endpoint Path"), ) one_click_token_secret = models.CharField( max_length=255, blank=True, - help_text="One-click token 簽章 secret;留空則使用 Django SECRET_KEY。", + help_text=_("One-click token signing secret. Leave blank to use Django SECRET_KEY."), + verbose_name=_("One-Click Token Secret"), ) - one_click_token_ttl_seconds = models.PositiveIntegerField(default=60 * 60 * 24 * 30) + one_click_token_ttl_seconds = models.PositiveIntegerField(default=60 * 60 * 24 * 30, verbose_name=_("One-Click Token TTL Seconds")) site_base_url = models.URLField( blank=True, - help_text="排程發送使用的站台網址(例如 https://news.example.com)。", + help_text=_("Site base URL for scheduler sends, e.g. https://news.example.com"), + verbose_name=_("Site Base URL"), ) panels = [ @@ -267,7 +286,7 @@ class NewsletterSystemSettings(BaseGenericSetting): FieldPanel("member_center_list_id"), FieldPanel("member_center_timeout_seconds"), ], - heading="Member Center", + heading=_("Member Center"), ), MultiFieldPanel( [ @@ -278,7 +297,7 @@ class NewsletterSystemSettings(BaseGenericSetting): FieldPanel("send_engine_retry_interval_seconds"), FieldPanel("send_engine_retry_max_attempts"), ], - heading="Send Engine", + heading=_("Send Engine"), ), MultiFieldPanel( [ @@ -287,7 +306,7 @@ class NewsletterSystemSettings(BaseGenericSetting): FieldPanel("one_click_token_ttl_seconds"), FieldPanel("site_base_url"), ], - heading="List-Unsubscribe One-Click", + heading=_("List-Unsubscribe One-Click"), ), MultiFieldPanel( [ @@ -296,12 +315,12 @@ class NewsletterSystemSettings(BaseGenericSetting): FieldPanel("reply_to_email"), FieldPanel("default_charset"), ], - heading="Newsletter Mail", + heading=_("Newsletter Mail"), ), ] class Meta: - verbose_name = "Newsletter System Settings" + verbose_name = _("Newsletter System Settings") def save(self, *args, **kwargs): if self.pk and not self.member_center_oauth_client_secret: @@ -315,18 +334,20 @@ class NewsletterSystemSettings(BaseGenericSetting): @register_setting class SystemNotificationMailSettings(BaseGenericSetting): - contact_form_from_name = models.CharField(max_length=255, blank=True) - contact_form_from_email = models.EmailField(blank=True) - contact_form_reply_to_email = models.EmailField(blank=True) + contact_form_from_name = models.CharField(max_length=255, blank=True, verbose_name=_("Contact Form Sender Name")) + contact_form_from_email = models.EmailField(blank=True, verbose_name=_("Contact Form Sender Email")) + contact_form_reply_to_email = models.EmailField(blank=True, verbose_name=_("Contact Form Reply-To Email")) contact_form_to_emails = models.TextField( blank=True, - help_text="可填多個收件人,以逗號或換行分隔。", + help_text=_("Multiple recipients separated by comma or newline."), + verbose_name=_("Contact Form Notification Recipients"), ) - contact_form_subject_prefix = models.CharField(max_length=255, blank=True, default="[Contact Us]") + contact_form_subject_prefix = models.CharField(max_length=255, blank=True, default="[Contact Us]", verbose_name=_("Contact Form Subject Prefix")) contact_form_user_subject_template = models.CharField( max_length=255, blank=True, default="已收到您的聯絡表單", + verbose_name=_("User Copy Subject Template"), ) contact_form_user_text_template = models.TextField( blank=True, @@ -338,6 +359,7 @@ class SystemNotificationMailSettings(BaseGenericSetting): "問題類別: {{category}}\n\n" "留言內容:\n{{message}}\n" ), + verbose_name=_("User Copy Text Template"), ) contact_form_user_html_template = models.TextField( blank=True, @@ -352,8 +374,9 @@ class SystemNotificationMailSettings(BaseGenericSetting): "

留言內容:

" "

{{message}}

" ), + verbose_name=_("User Copy HTML Template"), ) - default_charset = models.CharField(max_length=50, default="utf-8") + default_charset = models.CharField(max_length=50, default="utf-8", verbose_name=_("Default Charset")) panels = [ MultiFieldPanel( @@ -368,12 +391,12 @@ class SystemNotificationMailSettings(BaseGenericSetting): FieldPanel("contact_form_user_html_template"), FieldPanel("default_charset"), ], - heading="Contact Us Notification Mail", + heading=_("Contact Us Notification Mail"), ), ] class Meta: - verbose_name = "System Notification Mail Settings" + verbose_name = _("System Notification Mail Settings") def contact_form_recipient_list(self) -> list[str]: raw = (self.contact_form_to_emails or "").replace("\n", ",") @@ -404,24 +427,24 @@ class NewsletterCampaign(models.Model): STATUS_SENT = "sent" STATUS_FAILED = "failed" STATUS_CHOICES = [ - (STATUS_DRAFT, "Draft"), - (STATUS_SCHEDULED, "Scheduled"), - (STATUS_SENDING, "Sending"), - (STATUS_SENT, "Sent"), - (STATUS_FAILED, "Failed"), + (STATUS_DRAFT, _("Draft")), + (STATUS_SCHEDULED, _("Scheduled")), + (STATUS_SENDING, _("Sending")), + (STATUS_SENT, _("Sent")), + (STATUS_FAILED, _("Failed")), ] - title = models.CharField(max_length=255) - list_id = models.CharField(max_length=128, blank=True) - subject_template = models.CharField(max_length=255) - html_template = models.TextField() - text_template = models.TextField(blank=True) - status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_DRAFT) - scheduled_at = models.DateTimeField(null=True, blank=True) - sent_at = models.DateTimeField(null=True, blank=True) - last_error = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) + title = models.CharField(max_length=255, verbose_name=_("Title")) + list_id = models.CharField(max_length=128, blank=True, verbose_name=_("List ID")) + subject_template = models.CharField(max_length=255, verbose_name=_("Subject Template")) + html_template = models.TextField(verbose_name=_("HTML Template")) + text_template = models.TextField(blank=True, verbose_name=_("Text Template")) + status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_DRAFT, verbose_name=_("Status")) + scheduled_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Scheduled At")) + sent_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Sent At")) + last_error = models.TextField(blank=True, verbose_name=_("Last Error")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) + updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated At")) panels = [ FieldPanel("title"), @@ -434,6 +457,8 @@ class NewsletterCampaign(models.Model): class Meta: ordering = ["-created_at"] + verbose_name = _("Newsletter Campaign") + verbose_name_plural = _("Newsletter Campaigns") def __str__(self): return self.title @@ -451,16 +476,17 @@ class NewsletterDispatchRecord(models.Model): NewsletterCampaign, on_delete=models.CASCADE, related_name="dispatch_records", + verbose_name=_("Campaign"), ) - subscriber_id = models.CharField(max_length=128, blank=True) - email = models.EmailField(blank=True) - status = models.CharField(max_length=32, blank=True) - retry_count = models.PositiveIntegerField(default=0) - next_retry_at = models.DateTimeField(null=True, blank=True) - response_status = models.PositiveIntegerField(null=True, blank=True) - response_payload = models.JSONField(default=dict, blank=True) - error_message = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) + subscriber_id = models.CharField(max_length=128, blank=True, verbose_name=_("Subscriber ID")) + email = models.EmailField(blank=True, verbose_name=_("Email")) + status = models.CharField(max_length=32, blank=True, verbose_name=_("Status")) + retry_count = models.PositiveIntegerField(default=0, verbose_name=_("Retry Count")) + next_retry_at = models.DateTimeField(null=True, blank=True, verbose_name=_("Next Retry At")) + response_status = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Response Status")) + response_payload = models.JSONField(default=dict, blank=True, verbose_name=_("Response Payload")) + error_message = models.TextField(blank=True, verbose_name=_("Error Message")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) panels = [ FieldPanel("campaign"), @@ -476,6 +502,8 @@ class NewsletterDispatchRecord(models.Model): class Meta: ordering = ["-created_at"] + verbose_name = _("Newsletter Dispatch Record") + verbose_name_plural = _("Newsletter Dispatch Records") def __str__(self): return f"{self.campaign_id}:{self.email or self.subscriber_id}" @@ -486,35 +514,43 @@ class NewsletterTemplateSettings(BaseGenericSetting): subscribe_subject_template = models.CharField( max_length=255, default="請確認您的電子報訂閱", + verbose_name=_("Subscribe Subject Template"), ) subscribe_html_template = models.TextField( default=( "

您好,請點擊以下連結完成訂閱:

" "

{{confirm_url}}

" ), + verbose_name=_("Subscribe HTML Template"), ) subscribe_text_template = models.TextField( default="您好,請點擊以下連結完成訂閱:{{confirm_url}}", + verbose_name=_("Subscribe Text Template"), ) confirm_success_template = RichTextField( blank=True, default="

訂閱確認成功。

", + verbose_name=_("Confirm Success Template"), ) confirm_failure_template = RichTextField( blank=True, default="

訂閱確認失敗,請稍後再試。

", + verbose_name=_("Confirm Failure Template"), ) unsubscribe_intro_template = RichTextField( blank=True, default="

確認要退訂電子報嗎?

", + verbose_name=_("Unsubscribe Intro Template"), ) unsubscribe_success_template = RichTextField( blank=True, default="

已完成退訂。

", + verbose_name=_("Unsubscribe Success Template"), ) unsubscribe_failure_template = RichTextField( blank=True, default="

退訂失敗,請稍後再試。

", + verbose_name=_("Unsubscribe Failure Template"), ) panels = [ @@ -524,7 +560,7 @@ class NewsletterTemplateSettings(BaseGenericSetting): FieldPanel("subscribe_html_template"), FieldPanel("subscribe_text_template"), ], - heading="Subscribe Confirmation Email", + heading=_("Subscribe Confirmation Email"), ), MultiFieldPanel( [ @@ -534,18 +570,18 @@ class NewsletterTemplateSettings(BaseGenericSetting): FieldPanel("unsubscribe_success_template"), FieldPanel("unsubscribe_failure_template"), ], - heading="Page Templates", + heading=_("Page Templates"), ), ] class Meta: - verbose_name = "Newsletter Template Settings" + verbose_name = _("Newsletter Template Settings") @register_snippet class BannerSnippet(models.Model): - key = models.CharField(max_length=50, blank=True, help_text="識別用 key(例如 home / category)") - title = models.CharField(max_length=255, blank=True) + key = models.CharField(max_length=50, blank=True, help_text=_("Identifier key, e.g. home / category"), verbose_name=_("Key")) + title = models.CharField(max_length=255, blank=True, verbose_name=_("Title")) image = models.ForeignKey( "wagtailimages.Image", null=True, @@ -554,9 +590,9 @@ class BannerSnippet(models.Model): related_name="+", ) link_url = models.URLField(blank=True) - link_text = models.CharField(max_length=100, blank=True) - is_active = models.BooleanField(default=True) - sort_order = models.PositiveIntegerField(default=0) + link_text = models.CharField(max_length=100, blank=True, verbose_name=_("Link Text")) + is_active = models.BooleanField(default=True, verbose_name=_("Active")) + sort_order = models.PositiveIntegerField(default=0, verbose_name=_("Sort Order")) panels = [ FieldPanel("key"), @@ -570,8 +606,8 @@ class BannerSnippet(models.Model): class Meta: ordering = ["sort_order", "id"] - verbose_name = "Banner" - verbose_name_plural = "Banners" + verbose_name = _("Banner") + verbose_name_plural = _("Banners") def __str__(self): return self.title or f"Banner {self.pk}" @@ -602,7 +638,7 @@ class FooterText( return {"footer_text": self.body} class Meta(TranslatableMixin.Meta): - verbose_name_plural = "Footer Text" + verbose_name_plural = _("Footer Text") class ContactFormSubmission(models.Model): @@ -611,24 +647,26 @@ class ContactFormSubmission(models.Model): CATEGORY_CAREER = "career" CATEGORY_OTHER = "other" CATEGORY_CHOICES = [ - (CATEGORY_COLLABORATION, "合作邀約"), - (CATEGORY_WEBSITE_ISSUE, "網站問題回報"), - (CATEGORY_CAREER, "求職專區"), - (CATEGORY_OTHER, "其他"), + (CATEGORY_COLLABORATION, _("Collaboration")), + (CATEGORY_WEBSITE_ISSUE, _("Website Issue")), + (CATEGORY_CAREER, _("Career")), + (CATEGORY_OTHER, _("Other")), ] - name = models.CharField(max_length=100) - email = models.EmailField(blank=True) - contact = models.CharField(max_length=255) - category = models.CharField(max_length=32, choices=CATEGORY_CHOICES) - message = models.TextField() - source_page = models.CharField(max_length=512, blank=True) - ip_address = models.GenericIPAddressField(null=True, blank=True) - user_agent = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) + name = models.CharField(max_length=100, verbose_name=_("Name")) + email = models.EmailField(blank=True, verbose_name=_("Email")) + contact = models.CharField(max_length=255, verbose_name=_("Contact")) + category = models.CharField(max_length=32, choices=CATEGORY_CHOICES, verbose_name=_("Category")) + message = models.TextField(verbose_name=_("Message")) + source_page = models.CharField(max_length=512, blank=True, verbose_name=_("Source Page")) + ip_address = models.GenericIPAddressField(null=True, blank=True, verbose_name=_("IP Address")) + user_agent = models.TextField(blank=True, verbose_name=_("User Agent")) + created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created At")) class Meta: ordering = ["-created_at"] + verbose_name = _("Contact Form Submission") + verbose_name_plural = _("Contact Form Submissions") def __str__(self): return f"{self.get_category_display()} - {self.name}" diff --git a/innovedus_cms/base/wagtail_hooks.py b/innovedus_cms/base/wagtail_hooks.py index 9422f7f..ea610b7 100644 --- a/innovedus_cms/base/wagtail_hooks.py +++ b/innovedus_cms/base/wagtail_hooks.py @@ -1,5 +1,5 @@ from django.urls import reverse -from django.utils.translation import get_language +from django.utils.translation import gettext as _ from wagtail import hooks from wagtail.admin.panels import FieldPanel from wagtail.admin.rich_text import DraftailRichTextArea @@ -25,7 +25,7 @@ class NewsletterCampaignCreateView(CreateView): class NewsletterCampaignViewSet(SnippetViewSet): model = NewsletterCampaign icon = "mail" - menu_label = "Newsletter campaigns" + menu_label = _("Newsletter campaigns") menu_order = 250 add_to_admin_menu = True add_view_class = NewsletterCampaignCreateView @@ -36,7 +36,7 @@ class NewsletterCampaignViewSet(SnippetViewSet): FieldPanel("title"), FieldPanel( "list_id", - help_text="留空時會使用 Newsletter System Settings 的 member_center_list_id。", + help_text=_("Leave blank to use member_center_list_id from Newsletter System Settings."), ), FieldPanel("subject_template"), FieldPanel( @@ -44,7 +44,7 @@ class NewsletterCampaignViewSet(SnippetViewSet): widget=DraftailRichTextArea( features=["h2", "h3", "bold", "italic", "link", "image", "ul", "ol"], ), - help_text="可用圖片按鈕從 Wagtail 圖庫選圖或上傳。建議使用圖片 URL,不要在 CMS 端轉 Base64。", + help_text=_("Use image picker/upload from Wagtail. Prefer image URLs instead of Base64 in CMS."), ), FieldPanel("text_template"), FieldPanel("scheduled_at"), @@ -61,7 +61,7 @@ class ReadOnlySnippetPermissionPolicy(ModelPermissionPolicy): class NewsletterDispatchRecordViewSet(SnippetViewSet): model = NewsletterDispatchRecord icon = "tasks" - menu_label = "Newsletter dispatch records" + menu_label = _("Newsletter dispatch records") menu_order = 251 add_to_admin_menu = True inspect_view_enabled = True @@ -94,8 +94,7 @@ def newsletter_campaign_listing_buttons(snippet, user, next_url=None): if snippet.status == NewsletterCampaign.STATUS_SENDING: return - language = (get_language() or "").lower() - label = "馬上發送" if language.startswith("zh") else "Send now" + label = _("Send now") yield Button( label, reverse("newsletter_campaign_send_now", args=[snippet.pk]), diff --git a/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.mo b/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..77a1a3e08d98d18df28755afc8aed2af3dfb450f GIT binary patch literal 8770 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 {% endif %} + + {% if ga4_measurement_id %}