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

This commit is contained in:
Kailash Nadh 2021-01-23 19:34:30 +05:30
parent 4cd5e6ebeb
commit ee4fb7182f
14 changed files with 111 additions and 71 deletions

View file

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

View file

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

View file

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

View file

@ -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",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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