diff --git a/cmd/init.go b/cmd/init.go index 0dac9bc..3317ea1 100644 --- a/cmd/init.go +++ b/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) } diff --git a/cmd/main.go b/cmd/main.go index afb1ae3..7e11afa 100644 --- a/cmd/main.go +++ b/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 diff --git a/cmd/public.go b/cmd/public.go index ed3a48a..c553326 100644 --- a/cmd/public.go +++ b/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 { diff --git a/i18n/en.json b/i18n/en.json index 9d0972d..c1ef272 100644 --- a/i18n/en.json +++ b/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", diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go index fa958cf..391301a 100644 --- a/internal/i18n/i18n.go +++ b/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 } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index bf9b522..a166dfd 100644 --- a/internal/manager/manager.go +++ b/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 + }, } } diff --git a/static/email-templates/campaign-status.html b/static/email-templates/campaign-status.html index 8e52064..2fdb018 100644 --- a/static/email-templates/campaign-status.html +++ b/static/email-templates/campaign-status.html @@ -1,22 +1,22 @@ {{ define "campaign-status" }} {{ template "header" . }} -

{{ .L.T "email.status.campaignUpdate" }}

+

{{ L.Ts "email.status.campaignUpdate" }}

- + - + - + {{ if ne (index . "Reason") "" }} - + {{ end }} diff --git a/static/email-templates/default.tpl b/static/email-templates/default.tpl index 510340c..82653b1 100644 --- a/static/email-templates/default.tpl +++ b/static/email-templates/default.tpl @@ -77,8 +77,8 @@ diff --git a/static/email-templates/import-status.html b/static/email-templates/import-status.html index 6940dfc..5e9d543 100644 --- a/static/email-templates/import-status.html +++ b/static/email-templates/import-status.html @@ -1,17 +1,17 @@ {{ define "import-status" }} {{ template "header" . }} -

{{ .L.T "email.status.importTitle" }}

+

{{ L.Ts "email.status.importTitle" }}

{{ .L.T "globa.L.Terms.campaign" }}{{ L.Ts "globa.L.Terms.campaign" }} {{ index . "Name" }}
{{ .L.T "email.status.status" }}{{ L.Ts "email.status.status" }} {{ index . "Status" }}
{{ .L.T "email.status.campaignSent" }}{{ L.Ts "email.status.campaignSent" }} {{ index . "Sent" }} / {{ index . "ToSend" }}
{{ .L.T "email.status.campaignReason" }}{{ L.Ts "email.status.campaignReason" }} {{ index . "Reason" }}
- + - + - +
{{ .L.T "email.status.importFile" }}{{ L.Ts "email.status.importFile" }} {{ .Name }}
{{ .L.T "email.status.status" }}{{ L.Ts "email.status.status" }} {{ .Status }}
{{ .L.T "email.status.importRecords" }}{{ L.Ts "email.status.importRecords" }} {{ .Imported }} / {{ .Total }}
diff --git a/static/email-templates/subscriber-data.html b/static/email-templates/subscriber-data.html index 2ac644b..4a8c34d 100644 --- a/static/email-templates/subscriber-data.html +++ b/static/email-templates/subscriber-data.html @@ -1,8 +1,8 @@ {{ define "subscriber-data" }} {{ template "header" . }} -

{{ .L.T "email.data.title" }}

+

{{ L.Ts "email.data.title" }}

- {{ .L.T "email.data.info" }} + {{ L.Ts "email.data.info" }}

{{ template "footer" }} {{ end }} diff --git a/static/email-templates/subscriber-optin-campaign.html b/static/email-templates/subscriber-optin-campaign.html index 996939e..8477795 100644 --- a/static/email-templates/subscriber-optin-campaign.html +++ b/static/email-templates/subscriber-optin-campaign.html @@ -1,17 +1,17 @@ {{ define "optin-campaign" }} -

{{ .L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}

-

{{ .L.T "email.optin.confirmSubInfo" }}

+

{{ L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}

+

{{ L.Ts "email.optin.confirmSubInfo" }}

- {{ .L.T "email.optin.confirmSub" }} + {{ L.Ts "email.optin.confirmSub" }}

{{ end }} diff --git a/static/email-templates/subscriber-optin.html b/static/email-templates/subscriber-optin.html index 84be245..345c7a3 100644 --- a/static/email-templates/subscriber-optin.html +++ b/static/email-templates/subscriber-optin.html @@ -1,20 +1,20 @@ {{ define "subscriber-optin" }} {{ template "header" . }} -

{{ .L.T "email.optin.confirmSubTitle" }}

-

{{ .L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}

-

{{ .L.T "email.optin.confirmSubInfo" }}

+

{{ L.Ts "email.optin.confirmSubTitle" }}

+

{{ L.Ts "email.optin.confirmSubWelcome" "name" .Subscriber.FirstName }}

+

{{ L.Ts "email.optin.confirmSubInfo" }}

-

{{ .L.T "email.optin.confirmSubHelp" }}

+

{{ L.Ts "email.optin.confirmSubHelp" }}

- {{ .L.T "email.optin.confirmSub" }} + {{ L.Ts "email.optin.confirmSub" }}

{{ template "footer" }} diff --git a/static/public/templates/optin.html b/static/public/templates/optin.html index 692d58e..3bc7050 100644 --- a/static/public/templates/optin.html +++ b/static/public/templates/optin.html @@ -1,26 +1,26 @@ {{ define "optin" }} {{ template "header" .}}
-

{{ .L.T "public.confirmSubTitle" }}

+

{{ L.T "public.confirmSubTitle" }}

- {{ .L.T "public.confirmSubInfo" }} + {{ L.T "public.confirmSubInfo" }}

diff --git a/static/public/templates/subscription.html b/static/public/templates/subscription.html index 509dc89..32b4dba 100644 --- a/static/public/templates/subscription.html +++ b/static/public/templates/subscription.html @@ -1,18 +1,19 @@ {{ define "subscription" }} {{ template "header" .}}
-

Unsubscribe

-

Do you wish to unsubscribe from this mailing list?

+

{{ L.T "public.unsubTitle" }}

+

{{ L.T "public.unsubHelp" }}

{{ if .Data.AllowBlocklist }}

- + +

{{ end }}

- +

@@ -21,16 +22,16 @@ {{ if or .Data.AllowExport .Data.AllowWipe }}
-

Privacy and data

+

{{ L.T "public.privacyTitle" }}

{{ if .Data.AllowExport }}
- +
- A copy of your data will be e-mailed to you. + {{ L.T "public.privacyExportHelp" }}
{{ end }} @@ -41,14 +42,14 @@
- +
- Delete all your subscriptions and related data from our database permanently. + {{ L.T "public.privacyWipeHelp" }}
{{ end }}

- +

@@ -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; }