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=( "您好,請點擊以下連結完成訂閱:
" "" ), + 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 0000000..77a1a3e Binary files /dev/null and b/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.mo differ diff --git a/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.po b/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.po new file mode 100644 index 0000000..881337c --- /dev/null +++ b/innovedus_cms/locale/zh_Hant/LC_MESSAGES/django.po @@ -0,0 +1,432 @@ +msgid "" +msgstr "" +"Project-Id-Version: innovedus_cms\n" +"POT-Creation-Date: 2026-03-12 00:00+0000\n" +"PO-Revision-Date: 2026-03-12 00:00+0000\n" +"Last-Translator: Codex\n" +"Language-Team: zh_Hant\n" +"Language: zh_Hant\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "Header Settings" +msgstr "頁首設定" + +msgid "Use on dark background (light logo)." +msgstr "深色底用(亮色 logo)" + +msgid "Light Logo" +msgstr "亮色 Logo" + +msgid "Use on light background (dark logo)." +msgstr "淺色底用(深色 logo)" + +msgid "Dark Logo" +msgstr "深色 Logo" + +msgid "Site Name" +msgstr "網站名稱" + +msgid "Extra Links" +msgstr "額外連結" + +msgid "Label" +msgstr "標籤" + +msgid "URL" +msgstr "連結" + +msgid "Footer Navigation" +msgstr "頁尾導覽" + +msgid "Footer Links" +msgstr "頁尾連結" + +msgid "Section Title" +msgstr "區塊標題" + +msgid "Social Link" +msgstr "社群連結" + +msgid "Social Links" +msgstr "社群連結" + +msgid "Platform" +msgstr "平台" + +msgid "SMTP Settings" +msgstr "SMTP 設定" + +msgid "SMTP Relay Host" +msgstr "SMTP Relay Host" + +msgid "SMTP Relay Port" +msgstr "SMTP Relay Port" + +msgid "Use TLS" +msgstr "使用 TLS" + +msgid "Use SSL" +msgstr "使用 SSL" + +msgid "Port 465 usually uses SSL (Implicit TLS); port 587 usually uses STARTTLS (TLS)." +msgstr "465 常用 SSL(Implicit TLS);587 常用 STARTTLS(TLS)。" + +msgid "SMTP Timeout Seconds" +msgstr "SMTP 逾時秒數" + +msgid "SMTP Username" +msgstr "SMTP 帳號" + +msgid "SMTP Password" +msgstr "SMTP 密碼" + +msgid "Newsletter System Settings" +msgstr "電子報系統設定" + +msgid "Member Center" +msgstr "會員中心" + +msgid "Member Center Base URL" +msgstr "會員中心 Base URL" + +msgid "Subscribe Path" +msgstr "訂閱路徑" + +msgid "Confirm Path" +msgstr "確認路徑" + +msgid "Unsubscribe Token Path" +msgstr "退訂 Token 路徑" + +msgid "Unsubscribe Path" +msgstr "退訂路徑" + +msgid "Subscriptions Path" +msgstr "訂閱列表路徑" + +msgid "OAuth Token Path" +msgstr "OAuth Token 路徑" + +msgid "OAuth Client ID" +msgstr "OAuth Client ID" + +msgid "OAuth Client Secret" +msgstr "OAuth Client Secret" + +msgid "OAuth Scope" +msgstr "OAuth Scope" + +msgid "OAuth Audience" +msgstr "OAuth Audience" + +msgid "One-Click Unsubscribe Path" +msgstr "一鍵退訂路徑" + +msgid "Tenant ID" +msgstr "Tenant ID" + +msgid "List ID" +msgstr "List ID" + +msgid "Member Center Timeout Seconds" +msgstr "會員中心逾時秒數" + +msgid "Send Engine" +msgstr "發送引擎" + +msgid "Send Engine Base URL" +msgstr "發送引擎 Base URL" + +msgid "Send Jobs Path" +msgstr "發送任務路徑" + +msgid "Send Engine OAuth Scope" +msgstr "發送引擎 OAuth Scope" + +msgid "Send Engine Timeout Seconds" +msgstr "發送引擎逾時秒數" + +msgid "Retry Interval Seconds" +msgstr "重試間隔秒數" + +msgid "Retry Max Attempts" +msgstr "最大重試次數" + +msgid "Newsletter Mail" +msgstr "電子報寄件設定" + +msgid "Sender Name" +msgstr "寄件者名稱" + +msgid "Sender Email" +msgstr "寄件者 Email" + +msgid "Reply-To Email" +msgstr "回覆 Email" + +msgid "Default Charset" +msgstr "預設字元編碼" + +msgid "List-Unsubscribe One-Click" +msgstr "一鍵退訂設定" + +msgid "One-Click Endpoint Path" +msgstr "一鍵退訂端點路徑" + +msgid "One-Click Token Secret" +msgstr "一鍵退訂 Token 密鑰" + +msgid "One-click token signing secret. Leave blank to use Django SECRET_KEY." +msgstr "One-click token 簽章密鑰;留空則使用 Django SECRET_KEY。" + +msgid "One-Click Token TTL Seconds" +msgstr "一鍵退訂 Token 有效秒數" + +msgid "Site Base URL" +msgstr "站台 Base URL" + +msgid "Site base URL for scheduler sends, e.g. https://news.example.com" +msgstr "排程發送使用的站台網址(例如 https://news.example.com)。" + +msgid "System Notification Mail Settings" +msgstr "系統通知信設定" + +msgid "Contact Us Notification Mail" +msgstr "聯絡我們通知信" + +msgid "Contact Form Sender Name" +msgstr "聯絡表單寄件者名稱" + +msgid "Contact Form Sender Email" +msgstr "聯絡表單寄件者 Email" + +msgid "Contact Form Reply-To Email" +msgstr "聯絡表單回覆 Email" + +msgid "Contact Form Notification Recipients" +msgstr "聯絡表單通知收件人" + +msgid "Multiple recipients separated by comma or newline." +msgstr "可填多個收件人,以逗號或換行分隔。" + +msgid "Contact Form Subject Prefix" +msgstr "聯絡表單主旨前綴" + +msgid "User Copy Subject Template" +msgstr "使用者存檔信主旨模板" + +msgid "User Copy Text Template" +msgstr "使用者存檔信純文字模板" + +msgid "User Copy HTML Template" +msgstr "使用者存檔信 HTML 模板" + +msgid "Subscribe Confirmation Email" +msgstr "訂閱確認信" + +msgid "Page Templates" +msgstr "頁面模板" + +msgid "Newsletter Template Settings" +msgstr "電子報模板設定" + +msgid "Banner" +msgstr "橫幅" + +msgid "Banners" +msgstr "橫幅" + +msgid "Key" +msgstr "識別鍵" + +msgid "Identifier key, e.g. home / category" +msgstr "識別用 key(例如 home / category)" + +msgid "Title" +msgstr "標題" + +msgid "Link Text" +msgstr "連結文字" + +msgid "Active" +msgstr "啟用" + +msgid "Sort Order" +msgstr "排序" + +msgid "Footer Text" +msgstr "頁尾文字" + +msgid "Draft" +msgstr "草稿" + +msgid "Scheduled" +msgstr "已排程" + +msgid "Sending" +msgstr "寄送中" + +msgid "Sent" +msgstr "已送出" + +msgid "Failed" +msgstr "失敗" + +msgid "Newsletter Campaign" +msgstr "電子報" + +msgid "Newsletter Campaigns" +msgstr "電子報" + +msgid "Newsletter Dispatch Record" +msgstr "電子報發送紀錄" + +msgid "Newsletter Dispatch Records" +msgstr "電子報發送紀錄" + +msgid "Collaboration" +msgstr "合作邀約" + +msgid "Website Issue" +msgstr "網站問題回報" + +msgid "Career" +msgstr "求職專區" + +msgid "Other" +msgstr "其他" + +msgid "Name" +msgstr "姓名" + +msgid "Email" +msgstr "Email" + +msgid "Contact" +msgstr "聯絡方式" + +msgid "Category" +msgstr "問題類別" + +msgid "Message" +msgstr "留言內容" + +msgid "Source Page" +msgstr "來源頁面" + +msgid "IP Address" +msgstr "IP 位址" + +msgid "User Agent" +msgstr "User Agent" + +msgid "Created At" +msgstr "建立時間" + +msgid "Contact Form Submission" +msgstr "聯絡表單提交" + +msgid "Contact Form Submissions" +msgstr "聯絡表單提交" + +msgid "Newsletter campaigns" +msgstr "電子報" + +msgid "Newsletter dispatch records" +msgstr "電子報發送紀錄" + +msgid "Social Media Settings" +msgstr "社群媒體設定" + +msgid "Subject Template" +msgstr "主旨模板" + +msgid "HTML Template" +msgstr "HTML 模板" + +msgid "Text Template" +msgstr "純文字模板" + +msgid "Status" +msgstr "狀態" + +msgid "Scheduled At" +msgstr "排程時間" + +msgid "Sent At" +msgstr "發送時間" + +msgid "Last Error" +msgstr "最後錯誤" + +msgid "Updated At" +msgstr "更新時間" + +msgid "Campaign" +msgstr "活動" + +msgid "Subscriber ID" +msgstr "訂閱者 ID" + +msgid "Retry Count" +msgstr "重試次數" + +msgid "Next Retry At" +msgstr "下次重試時間" + +msgid "Response Status" +msgstr "回應狀態碼" + +msgid "Response Payload" +msgstr "回應內容" + +msgid "Error Message" +msgstr "錯誤訊息" + +msgid "Subscribe Subject Template" +msgstr "訂閱主旨模板" + +msgid "Subscribe HTML Template" +msgstr "訂閱 HTML 模板" + +msgid "Subscribe Text Template" +msgstr "訂閱純文字模板" + +msgid "Confirm Success Template" +msgstr "確認成功模板" + +msgid "Confirm Failure Template" +msgstr "確認失敗模板" + +msgid "Unsubscribe Intro Template" +msgstr "退訂前置說明模板" + +msgid "Unsubscribe Success Template" +msgstr "退訂成功模板" + +msgid "Unsubscribe Failure Template" +msgstr "退訂失敗模板" + +msgid "Leave blank to use member_center_list_id from Newsletter System Settings." +msgstr "留空時會使用 Newsletter System Settings 的 member_center_list_id。" + +msgid "Use image picker/upload from Wagtail. Prefer image URLs instead of Base64 in CMS." +msgstr "可用圖片按鈕從 Wagtail 圖庫選圖或上傳。建議使用圖片 URL,不要在 CMS 端轉 Base64。" + +msgid "Send now" +msgstr "馬上發送" + +msgid "Send test email (save settings before sending)." +msgstr "發送測試郵件(請先儲存設定後再發送測試郵件)" + +msgid "Send email to" +msgstr "發送郵件到" + +msgid "This field is only for this test and will not be saved." +msgstr "此欄位僅作本次測試,不會儲存。" + +msgid "Send test email" +msgstr "發送測試郵件" diff --git a/innovedus_cms/mysite/settings/base.py b/innovedus_cms/mysite/settings/base.py index 128f2b9..2a777b5 100644 --- a/innovedus_cms/mysite/settings/base.py +++ b/innovedus_cms/mysite/settings/base.py @@ -147,6 +147,10 @@ USE_I18N = True USE_TZ = True +LOCALE_PATHS = [ + os.path.join(BASE_DIR, "locale"), +] + # --- Wagtail embeds (Instagram/FB via Graph oEmbed) --- # Reads a token from env and adds an extra oEmbed finder for IG/FB. # Keeps the default oEmbed finder first for YouTube/Vimeo/etc. diff --git a/innovedus_cms/mysite/static/favicon.ico b/innovedus_cms/mysite/static/favicon.ico new file mode 100644 index 0000000..c68e8db Binary files /dev/null and b/innovedus_cms/mysite/static/favicon.ico differ diff --git a/innovedus_cms/mysite/templates/base.html b/innovedus_cms/mysite/templates/base.html index 41ed983..4d454d8 100644 --- a/innovedus_cms/mysite/templates/base.html +++ b/innovedus_cms/mysite/templates/base.html @@ -17,6 +17,8 @@ {% endif %} + + {% if ga4_measurement_id %}