Browse Source

Refactor i18n name and fix the L() function in public HTML templates

Kailash Nadh 4 years ago
parent
commit
ee4fb7182f

+ 9 - 6
cmd/init.go

@@ -258,13 +258,13 @@ func initConstants() *constants {
 
 // initI18n initializes a new i18n instance with the selected language map
 // loaded from the filesystem.
-func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18nLang {
+func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
 	b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
 	if err != nil {
 		lo.Fatalf("error loading i18n language file: %v", err)
 	}
 
-	i, err := i18n.New(lang, b)
+	i, err := i18n.New(b)
 	if err != nil {
 		lo.Fatalf("error unmarshalling i18n language: %v", err)
 	}
@@ -298,7 +298,7 @@ func initCampaignManager(q *Queries, cs *constants, app *App) *manager.Manager {
 		ViewTrackURL:       cs.ViewTrackURL,
 		MessageURL:         cs.MessageURL,
 		UnsubHeader:        ko.Bool("privacy.unsubscribe_header"),
-	}, newManagerDB(q), campNotifCB, lo)
+	}, newManagerDB(q), campNotifCB, app.i18n, lo)
 
 }
 
@@ -428,7 +428,7 @@ func initMediaStore() media.Store {
 
 // initNotifTemplates compiles and returns e-mail notification templates that are
 // used for sending ad-hoc notifications to admins and subscribers.
-func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18nLang, cs *constants) *template.Template {
+func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18n, cs *constants) *template.Template {
 	// Register utility functions that the e-mail templates can use.
 	funcs := template.FuncMap{
 		"RootURL": func() string {
@@ -437,7 +437,7 @@ func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18nLang, c
 		"LogoURL": func() string {
 			return cs.LogoURL
 		},
-		"L": func() *i18n.I18nLang {
+		"L": func() *i18n.I18n {
 			return i
 		},
 	}
@@ -464,7 +464,10 @@ func initHTTPServer(app *App) *echo.Echo {
 	})
 
 	// Parse and load user facing templates.
-	tpl, err := stuffbin.ParseTemplatesGlob(nil, app.fs, "/public/templates/*.html")
+	tpl, err := stuffbin.ParseTemplatesGlob(template.FuncMap{
+		"L": func() *i18n.I18n {
+			return app.i18n
+		}}, app.fs, "/public/templates/*.html")
 	if err != nil {
 		lo.Fatalf("error parsing public templates: %v", err)
 	}

+ 1 - 1
cmd/main.go

@@ -40,7 +40,7 @@ type App struct {
 	importer   *subimporter.Importer
 	messengers map[string]messenger.Messenger
 	media      media.Store
-	i18n       *i18n.I18nLang
+	i18n       *i18n.I18n
 	notifTpls  *template.Template
 	log        *log.Logger
 	bufLog     *buflog.BufLog

+ 1 - 1
cmd/public.go

@@ -39,7 +39,7 @@ type tplData struct {
 	LogoURL    string
 	FaviconURL string
 	Data       interface{}
-	L          *i18n.I18nLang
+	L          *i18n.I18n
 }
 
 type publicTpl struct {

+ 13 - 1
i18n/en.json

@@ -83,6 +83,7 @@
     "globals.buttons.cancel": "Cancel",
     "globals.buttons.clone": "Clone",
     "globals.buttons.close": "Close",
+    "globals.buttons.continue": "Continue",
     "globals.buttons.delete": "Delete",
     "globals.buttons.edit": "Edit",
     "globals.buttons.enabled": "Enabled",
@@ -188,14 +189,25 @@
     "menu.media": "Media",
     "menu.newCampaign": "Create new",
     "menu.settings": "Settings",
+    "public.subNotFound": "Subscription not found.",
     "public.campaignNotFound": "The e-mail message was not found.",
+    "public.unsubTitle": "Unsubscribe",
+    "public.unsubHelp": "Do you want to unsubscribe from this mailing list?",
+    "public.unsubFull": "Also unsubscribe from all future e-mails.",
+    "public.unsub": "Unsubscribe",
+    "public.privacyTitle": "Privacy and data",
+    "public.privacyExport": "Export your data",
+    "public.privacyExportHelp": "A copy of your data will be e-mailed to you.",
+    "public.privacyWipe": "Wipe your data",
+    "public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.",
+    "public.privacyConfirmWipe": "Are you sure you want to delete all your subscription data permanently?",
     "public.confirmOptinSubTitle": "Confirm subscription",
     "public.confirmSub": "Confirm subscription",
     "public.confirmSubInfo": "You have been added to the following lists:",
     "public.confirmSubTitle": "Confirm",
     "public.dataRemoved": "Your subscriptions and all associated data has been removed.",
     "public.dataRemovedTitle": "Data removed",
-    "public.dataSent": "Your data has been e-mailed to you as an attachment",
+    "public.dataSent": "Your data has been e-mailed to you as an attachment.",
     "public.dataSentTitle": "Data e-mailed",
     "public.errorFetchingCampaign": "Error fetching e-mail message",
     "public.errorFetchingEmail": "E-mail message not found",

+ 40 - 22
internal/i18n/i18n.go

@@ -1,48 +1,66 @@
+// i18n is a simple package that translates strings using a language map.
+// It mimicks some functionality of the vue-i18n library so that the same JSON
+// language map may be used in the JS frontent and the Go backend.
 package i18n
 
 import (
 	"encoding/json"
+	"errors"
 	"regexp"
 	"strings"
 )
 
-// Lang represents a loaded language.
-type Lang struct {
-	Code    string `json:"code"`
-	Name    string `json:"name"`
-	langMap map[string]string
-}
-
-// I18nLang is a simple i18n library that translates strings using a language map.
-// It mimicks some functionality of the vue-i18n library so that the same JSON
-// language map may be used in the JS frontent and the Go backend.
-type I18nLang struct {
-	Code    string `json:"code"`
-	Name    string `json:"name"`
+// I18n offers translation functions over a language map.
+type I18n struct {
+	code    string `json:"code"`
+	name    string `json:"name"`
 	langMap map[string]string
 }
 
 var reParam = regexp.MustCompile(`(?i)\{([a-z0-9-.]+)\}`)
 
 // New returns an I18n instance.
-func New(code string, b []byte) (*I18nLang, error) {
+func New(b []byte) (*I18n, error) {
 	var l map[string]string
 	if err := json.Unmarshal(b, &l); err != nil {
 		return nil, err
 	}
-	return &I18nLang{
+
+	code, ok := l["_.code"]
+	if !ok {
+		return nil, errors.New("missing _.code field in language file")
+	}
+
+	name, ok := l["_.name"]
+	if !ok {
+		return nil, errors.New("missing _.name field in language file")
+	}
+
+	return &I18n{
 		langMap: l,
+		code:    code,
+		name:    name,
 	}, nil
 }
 
+// Name returns the canonical name of the language.
+func (i *I18n) Name() string {
+	return i.name
+}
+
+// Code returns the ISO code of the language.
+func (i *I18n) Code() string {
+	return i.code
+}
+
 // JSON returns the languagemap as raw JSON.
-func (i *I18nLang) JSON() []byte {
+func (i *I18n) JSON() []byte {
 	b, _ := json.Marshal(i.langMap)
 	return b
 }
 
 // T returns the translation for the given key similar to vue i18n's t().
-func (i *I18nLang) T(key string) string {
+func (i *I18n) T(key string) string {
 	s, ok := i.langMap[key]
 	if !ok {
 		return key
@@ -59,7 +77,7 @@ func (i *I18nLang) T(key string) string {
 // eg: Ts("globals.message.notFound",
 //         "name", "campaigns",
 //         "error", err)
-func (i *I18nLang) Ts(key string, params ...string) string {
+func (i *I18n) Ts(key string, params ...string) string {
 	if len(params)%2 != 0 {
 		return key + `: Invalid arguments`
 	}
@@ -82,7 +100,7 @@ func (i *I18nLang) Ts(key string, params ...string) string {
 // Tc returns the translation for the given key similar to vue i18n's tc().
 // It expects the language string in the map to be of the form `Singular | Plural` and
 // returns `Plural` if n > 1, or `Singular` otherwise.
-func (i *I18nLang) Tc(key string, n int) string {
+func (i *I18n) Tc(key string, n int) string {
 	s, ok := i.langMap[key]
 	if !ok {
 		return key
@@ -98,7 +116,7 @@ func (i *I18nLang) Tc(key string, n int) string {
 
 // getSingular returns the singular term from the vuei18n pipe separated value.
 // singular term | plural term
-func (i *I18nLang) getSingular(s string) string {
+func (i *I18n) getSingular(s string) string {
 	if !strings.Contains(s, "|") {
 		return s
 	}
@@ -108,7 +126,7 @@ func (i *I18nLang) getSingular(s string) string {
 
 // getSingular returns the plural term from the vuei18n pipe separated value.
 // singular term | plural term
-func (i *I18nLang) getPlural(s string) string {
+func (i *I18n) getPlural(s string) string {
 	if !strings.Contains(s, "|") {
 		return s
 	}
@@ -122,7 +140,7 @@ func (i *I18nLang) getPlural(s string) string {
 }
 
 // subAllParams recursively resolves and replaces all {params} in a string.
-func (i *I18nLang) subAllParams(s string) string {
+func (i *I18n) subAllParams(s string) string {
 	if !strings.Contains(s, `{`) {
 		return s
 	}

+ 7 - 1
internal/manager/manager.go

@@ -11,6 +11,7 @@ import (
 	"sync"
 	"time"
 
+	"github.com/knadh/listmonk/internal/i18n"
 	"github.com/knadh/listmonk/internal/messenger"
 	"github.com/knadh/listmonk/models"
 )
@@ -40,6 +41,7 @@ type DataSource interface {
 type Manager struct {
 	cfg        Config
 	src        DataSource
+	i18n       *i18n.I18n
 	messengers map[string]messenger.Messenger
 	notifCB    models.AdminNotifCallback
 	logger     *log.Logger
@@ -108,7 +110,7 @@ type msgError struct {
 }
 
 // New returns a new instance of Mailer.
-func New(cfg Config, src DataSource, notifCB models.AdminNotifCallback, l *log.Logger) *Manager {
+func New(cfg Config, src DataSource, notifCB models.AdminNotifCallback, i *i18n.I18n, l *log.Logger) *Manager {
 	if cfg.BatchSize < 1 {
 		cfg.BatchSize = 1000
 	}
@@ -122,6 +124,7 @@ func New(cfg Config, src DataSource, notifCB models.AdminNotifCallback, l *log.L
 	return &Manager{
 		cfg:                cfg,
 		src:                src,
+		i18n:               i,
 		notifCB:            notifCB,
 		logger:             l,
 		messengers:         make(map[string]messenger.Messenger),
@@ -334,6 +337,9 @@ func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
 			}
 			return time.Now().Format(layout)
 		},
+		"L": func() *i18n.I18n {
+			return m.i18n
+		},
 	}
 }
 

+ 5 - 5
static/email-templates/campaign-status.html

@@ -1,22 +1,22 @@
 {{ define "campaign-status" }}
 {{ template "header" . }}
-<h2>{{ .L.T "email.status.campaignUpdate" }}</h2>
+<h2>{{ L.Ts "email.status.campaignUpdate" }}</h2>
 <table width="100%">
     <tr>
-        <td width="30%"><strong>{{ .L.T "globa.L.Terms.campaign" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "globa.L.Terms.campaign" }}</strong></td>
         <td><a href="{{ index . "RootURL" }}/campaigns/{{ index . "ID" }}">{{ index . "Name" }}</a></td>
     </tr>
     <tr>
-        <td width="30%"><strong>{{ .L.T "email.status.status" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "email.status.status" }}</strong></td>
         <td>{{ index . "Status" }}</td>
     </tr>
     <tr>
-        <td width="30%"><strong>{{ .L.T "email.status.campaignSent" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "email.status.campaignSent" }}</strong></td>
         <td>{{ index . "Sent" }} / {{ index . "ToSend" }}</td>
     </tr>
     {{ if ne (index . "Reason") "" }}
         <tr>
-            <td width="30%"><strong>{{ .L.T "email.status.campaignReason" }}</strong></td>
+            <td width="30%"><strong>{{ L.Ts "email.status.campaignReason" }}</strong></td>
             <td>{{ index . "Reason" }}</td>
         </tr>
     {{ end }}

+ 2 - 2
static/email-templates/default.tpl

@@ -77,8 +77,8 @@
     
     <div class="footer" style="text-align: center;font-size: 12px;color: #888;">
         <p>
-            {{ I18n.T "email.unsubHelp" }}
-            <a href="{{ UnsubscribeURL }}" style="color: #888;">{{ I18n.T "email.unsub" }}</a>
+            {{ L.T "email.unsubHelp" }}
+            <a href="{{ UnsubscribeURL }}" style="color: #888;">{{ L.T "email.unsub" }}</a>
         </p>
         <p>Powered by <a href="https://listmonk.app" target="_blank" style="color: #888;">listmonk</a></p>
     </div>

+ 4 - 4
static/email-templates/import-status.html

@@ -1,17 +1,17 @@
 {{ define "import-status" }}
 {{ template "header" . }}
-<h2>{{ .L.T "email.status.importTitle" }}</h2>
+<h2>{{ L.Ts "email.status.importTitle" }}</h2>
 <table width="100%">
     <tr>
-        <td width="30%"><strong>{{ .L.T "email.status.importFile" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "email.status.importFile" }}</strong></td>
         <td><a href="{{ RootURL }}/subscribers/import">{{ .Name }}</a></td>
     </tr>
     <tr>
-        <td width="30%"><strong>{{ .L.T "email.status.status" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "email.status.status" }}</strong></td>
         <td>{{ .Status }}</td>
     </tr>
     <tr>
-        <td width="30%"><strong>{{ .L.T "email.status.importRecords" }}</strong></td>
+        <td width="30%"><strong>{{ L.Ts "email.status.importRecords" }}</strong></td>
         <td>{{ .Imported }} / {{ .Total }}</td>
     </tr>
 </table>

+ 2 - 2
static/email-templates/subscriber-data.html

@@ -1,8 +1,8 @@
 {{ define "subscriber-data" }}
 {{ template "header" . }}
-<h2>{{ .L.T "email.data.title" }}</h2>
+<h2>{{ L.Ts "email.data.title" }}</h2>
 <p>
-  {{ .L.T "email.data.info" }}
+  {{ L.Ts "email.data.info" }}
 </p>
 {{ template "footer" }}
 {{ end }}

+ 4 - 4
static/email-templates/subscriber-optin-campaign.html

@@ -1,17 +1,17 @@
 {{ define "optin-campaign" }}
 
-<p>{{ .L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}</p>
-<p>{{ .L.T "email.optin.confirmSubInfo" }}</p>
+<p>{{ L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}</p>
+<p>{{ L.Ts "email.optin.confirmSubInfo" }}</p>
 <ul>
     {{ range $i, $l := .Lists }}
         {{ if eq .Type "public" }}
             <li>{{ .Name }}</li>
         {{ else }}
-            <li>{{ .L.T "email.optin.privateList" }}</li>
+            <li>{{ L.Ts "email.optin.privateList" }}</li>
         {{ end }}
     {{ end }}
 </ul>
 <p>
-    <a class="button" {{ .OptinURLAttr }} class="button">{{ .L.T "email.optin.confirmSub" }}</a>
+    <a class="button" {{ .OptinURLAttr }} class="button">{{ L.Ts "email.optin.confirmSub" }}</a>
 </p>
 {{ end }}

+ 6 - 6
static/email-templates/subscriber-optin.html

@@ -1,20 +1,20 @@
 {{ define "subscriber-optin" }}
 {{ template "header" . }}
-<h2>{{ .L.T "email.optin.confirmSubTitle" }}</h2>
-<p>{{ .L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}</p>
-<p>{{ .L.T "email.optin.confirmSubInfo" }}</p>
+<h2>{{ L.Ts "email.optin.confirmSubTitle" }}</h2>
+<p>{{ L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}</p>
+<p>{{ L.Ts "email.optin.confirmSubInfo" }}</p>
 <ul>
     {{ range $i, $l := .Lists }}
         {{ if eq .Type "public" }}
             <li>{{ .Name }}</li>
         {{ else }}
-            <li>{{ .L.T "email.optin.privateList" }}</li>
+            <li>{{ L.Ts "email.optin.privateList" }}</li>
         {{ end }}
     {{ end }}
 </ul>
-<p>{{ .L.T "email.optin.confirmSubHelp" }}</p>
+<p>{{ L.Ts "email.optin.confirmSubHelp" }}</p>
 <p>
-    <a href="{{ .OptinURL }}" class="button">{{ .L.T "email.optin.confirmSub" }}</a>
+    <a href="{{ .OptinURL }}" class="button">{{ L.Ts "email.optin.confirmSub" }}</a>
 </p>
 
 {{ template "footer" }}

+ 5 - 5
static/public/templates/optin.html

@@ -1,26 +1,26 @@
 {{ define "optin" }}
 {{ template "header" .}}
 <section>
-    <h2>{{ .L.T "public.confirmSubTitle" }}</h2>
+    <h2>{{ L.T "public.confirmSubTitle" }}</h2>
     <p>
-        {{ .L.T "public.confirmSubInfo" }}
+        {{ L.T "public.confirmSubInfo" }}
     </p>
 
     <form method="post">
         <ul>
             {{ range $i, $l := .Data.Lists }}
                 <input type="hidden" name="l" value="{{ $l.UUID }}" />
-                {{ if eq $.L.Type "public" }}
+                {{ if eq $l.Type "public" }}
                     <li>{{ $l.Name }}</li>
                 {{ else }}
-                    <li>{{ .L.T "public.subPrivateList" }}</li>
+                    <li>{{ L.Ts "public.subPrivateList" }}</li>
                 {{ end }}
             {{ end }}
         </ul>
         <p>
             <input type="hidden" name="confirm" value="true" />
             <button type="submit" class="button" id="btn-unsub">
-                {{ .L.T "public.confirmSub" }}
+                {{ L.Ts "public.confirmSub" }}
             </button>
         </p>
     </form>

+ 12 - 11
static/public/templates/subscription.html

@@ -1,18 +1,19 @@
 {{ define "subscription" }}
 {{ template "header" .}}
 <section>
-    <h2>Unsubscribe</h2>
-    <p>Do you wish to unsubscribe from this mailing list?</p>
+    <h2>{{ L.T "public.unsubTitle" }}</h2>
+    <p>{{ L.T "public.unsubHelp" }}</p>
     <form method="post">
         <div>
             {{ if .Data.AllowBlocklist }}
                 <p>
-                    <input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" /> <label for="privacy-blocklist">Also unsubscribe from all future e-mails.</label>
+                    <input id="privacy-blocklist" type="checkbox" name="blocklist" value="true" />
+                    <label for="privacy-blocklist">{{ L.T "public.unsubFull" }}</label>
                 </p>
             {{ end }}
 
             <p>
-                <button type="submit" class="button" id="btn-unsub">Unsubscribe</button>
+                <button type="submit" class="button" id="btn-unsub">{{ L.T "public.unsub" }}</button>
             </p>
         </div>
     </form>
@@ -21,16 +22,16 @@
 {{ if or .Data.AllowExport .Data.AllowWipe }}
 <form id="data-form" method="post" action="" onsubmit="return handleData()">
     <section>
-        <h2>Privacy and data</h2>
+        <h2>{{ L.T "public.privacyTitle" }}</h2>
         {{ if .Data.AllowExport }}
         <div class="row">
             <div class="one columns">
                 <input id="privacy-export" type="radio" name="data-action" value="export" required />
             </div>
             <div class="ten columns">
-                <label for="privacy-export"><strong>Export your data</strong></label>
+                <label for="privacy-export"><strong>{{ L.T "public.privacyExport" }}</strong></label>
                 <br />
-                A copy of your data will be e-mailed to you.
+                {{ L.T "public.privacyExportHelp" }}
             </div>
         </div>
         {{ end }}
@@ -41,14 +42,14 @@
                 <input id="privacy-wipe" type="radio" name="data-action" value="wipe" required />
             </div>
             <div class="ten columns">
-                <label for="privacy-wipe"><strong>Wipe your data</strong></label>
+                <label for="privacy-wipe"><strong>{{ L.T "public.privacyWipe" }}</strong></label>
                 <br />
-                Delete all your subscriptions and related data from our database permanently.
+                {{ L.T "public.privacyWipeHelp" }}
             </div>
         </div>
         {{ end }}
         <p>
-            <input type="submit" value="Continue" class="button button-outline" />
+            <input type="submit" value="{{ L.T "globals.buttons.continue" }}" class="button button-outline" />
         </p>
     </section>
 </form>
@@ -59,7 +60,7 @@
         if (a == "export") {
             f.action = "/subscription/export/{{ .Data.SubUUID }}";
             return true;
-        } else if (confirm("Are you sure you want to delete all your subscription data permanently?")) {
+        } else if (confirm("{{ L.T "public.privacyConfirmWipe" }}")) {
             f.action = "/subscription/wipe/{{ .Data.SubUUID }}";
             return true;
         }