diff --git a/cmd/admin.go b/cmd/admin.go index 289ef20..292226b 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -1,8 +1,6 @@ package main import ( - "bytes" - "encoding/json" "fmt" "net/http" "sort" @@ -13,41 +11,29 @@ import ( "github.com/labstack/echo" ) -type configScript struct { - RootURL string `json:"rootURL"` - FromEmail string `json:"fromEmail"` - Messengers []string `json:"messengers"` - MediaProvider string `json:"mediaProvider"` - NeedsRestart bool `json:"needsRestart"` - Update *AppUpdate `json:"update"` - Langs []i18nLang `json:"langs"` - EnablePublicSubPage bool `json:"enablePublicSubscriptionPage"` - Lang json.RawMessage `json:"lang"` +type serverConfig struct { + Messengers []string `json:"messengers"` + Langs []i18nLang `json:"langs"` + Lang string `json:"lang"` + Update *AppUpdate `json:"update"` + NeedsRestart bool `json:"needs_restart"` } -// handleGetConfigScript returns general configuration as a Javascript -// variable that can be included in an HTML page directly. -func handleGetConfigScript(c echo.Context) error { +// handleGetServerConfig returns general server config. +func handleGetServerConfig(c echo.Context) error { var ( app = c.Get("app").(*App) - out = configScript{ - RootURL: app.constants.RootURL, - FromEmail: app.constants.FromEmail, - MediaProvider: app.constants.MediaProvider, - EnablePublicSubPage: app.constants.EnablePublicSubPage, - } + out = serverConfig{} ) // Language list. - langList, err := geti18nLangList(app.constants.Lang, app) + langList, err := getI18nLangList(app.constants.Lang, app) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Error loading language list: %v", err)) } out.Langs = langList - - // Current language. - out.Lang = json.RawMessage(app.i18n.JSON()) + out.Lang = app.constants.Lang // Sort messenger names with `email` always as the first item. var names []string @@ -66,17 +52,7 @@ func handleGetConfigScript(c echo.Context) error { out.Update = app.update app.Unlock() - // Write the Javascript variable opening; - b := bytes.Buffer{} - b.Write([]byte(`var CONFIG = `)) - - // Encode the config payload as JSON and write as the variable's value assignment. - j := json.NewEncoder(&b) - if err := j.Encode(out); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, - app.i18n.Ts("admin.errorMarshallingConfig", "error", err.Error())) - } - return c.Blob(http.StatusOK, "application/javascript; charset=utf-8", b.Bytes()) + return c.JSON(http.StatusOK, okResp{out}) } // handleGetDashboardCharts returns chart data points to render ont he dashboard. diff --git a/cmd/handlers.go b/cmd/handlers.go index a399460..b1eaef0 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -2,8 +2,6 @@ package main import ( "crypto/subtle" - "encoding/json" - "fmt" "net/http" "net/url" "regexp" @@ -44,8 +42,8 @@ func registerHTTPHandlers(e *echo.Echo) { g := e.Group("", middleware.BasicAuth(basicAuth)) g.GET("/", handleIndexPage) g.GET("/api/health", handleHealthCheck) - g.GET("/api/config.js", handleGetConfigScript) - g.GET("/api/lang/:lang", handleLoadLanguage) + g.GET("/api/config", handleGetServerConfig) + g.GET("/api/lang/:lang", handleGetI18nLang) g.GET("/api/dashboard/charts", handleGetDashboardCharts) g.GET("/api/dashboard/counts", handleGetDashboardCounts) @@ -164,23 +162,6 @@ func handleHealthCheck(c echo.Context) error { return c.JSON(http.StatusOK, okResp{true}) } -// handleLoadLanguage returns the JSON language pack given the language code. -func handleLoadLanguage(c echo.Context) error { - app := c.Get("app").(*App) - - lang := c.Param("lang") - if len(lang) > 6 || reLangCode.MatchString(lang) { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.") - } - - b, err := app.fs.Read(fmt.Sprintf("/lang/%s.json", lang)) - if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.") - } - - return c.JSON(http.StatusOK, okResp{json.RawMessage(b)}) -} - // basicAuth middleware does an HTTP BasicAuth authentication for admin handlers. func basicAuth(username, password string, c echo.Context) (bool, error) { app := c.Get("app").(*App) diff --git a/cmd/i18n.go b/cmd/i18n.go index f91b2f8..66bb0e9 100644 --- a/cmd/i18n.go +++ b/cmd/i18n.go @@ -3,6 +3,11 @@ package main import ( "encoding/json" "fmt" + "net/http" + + "github.com/knadh/listmonk/internal/i18n" + "github.com/knadh/stuffbin" + "github.com/labstack/echo" ) type i18nLang struct { @@ -15,8 +20,25 @@ type i18nLangRaw struct { Name string `json:"_.name"` } -// geti18nLangList returns the list of available i18n languages. -func geti18nLangList(lang string, app *App) ([]i18nLang, error) { +// handleGetI18nLang returns the JSON language pack given the language code. +func handleGetI18nLang(c echo.Context) error { + app := c.Get("app").(*App) + + lang := c.Param("lang") + if len(lang) > 6 || reLangCode.MatchString(lang) { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.") + } + + i, err := getI18nLang(lang, app.fs) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.") + } + + return c.JSON(http.StatusOK, okResp{json.RawMessage(i.JSON())}) +} + +// getI18nLangList returns the list of available i18n languages. +func getI18nLangList(lang string, app *App) ([]i18nLang, error) { list, err := app.fs.Glob("/i18n/*.json") if err != nil { return nil, err @@ -42,3 +64,30 @@ func geti18nLangList(lang string, app *App) ([]i18nLang, error) { return out, nil } + +func getI18nLang(lang string, fs stuffbin.FileSystem) (*i18n.I18n, error) { + const def = "en" + + b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def)) + if err != nil { + return nil, fmt.Errorf("error reading default i18n language file: %s: %v", def, err) + } + + // Initialize with the default language. + i, err := i18n.New(b) + if err != nil { + return nil, fmt.Errorf("error unmarshalling i18n language: %v", err) + } + + // Load the selected language on top of it. + b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang)) + if err != nil { + return nil, fmt.Errorf("error reading i18n language file: %v", err) + } + + if err := i.Load(b); err != nil { + return nil, fmt.Errorf("error loading i18n language file: %v", err) + } + + return i, nil +} diff --git a/cmd/init.go b/cmd/init.go index fab4fee..9170f9b 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -262,28 +262,10 @@ func initConstants() *constants { // and then the selected language is loaded on top of it so that if there are // missing translations in it, the default English translations show up. func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n { - const def = "en" - - b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def)) + i, err := getI18nLang(lang, fs) if err != nil { - lo.Fatalf("error reading default i18n language file: %s: %v", def, err) + lo.Fatal(err) } - - // Initialize with the default language. - i, err := i18n.New(b) - if err != nil { - lo.Fatalf("error unmarshalling i18n language: %v", err) - } - - // Load the selected language on top of it. - b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang)) - if err != nil { - lo.Fatalf("error reading i18n language file: %v", err) - } - if err := i.Load(b); err != nil { - lo.Fatalf("error loading i18n language file: %v", err) - } - return i } diff --git a/frontend/README.md b/frontend/README.md index 51964ae..85ae477 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,7 +5,10 @@ It's best if the `listmonk/frontend` directory is opened in an IDE as a separate For developer setup instructions, refer to the main project's README. ## Globals -`main.js` is where Buefy is injected globally into Vue. In addition two controllers, `$api` (collection of API calls from `api/index.js`), `$utils` (util functions from `util.js`), `$serverConfig` (loaded form /api/config.js) are also attached globaly to Vue. They are accessible within Vue as `this.$api` and `this.$utils`. +In `main.js`, Buefy and vue-i18n are attached globally. In addition: + +- `$api` (collection of API calls from `api/index.js`) +- `$utils` (util functions from `util.js`). They are accessible within Vue as `this.$api` and `this.$utils`. Some constants are defined in `constants.js`. @@ -14,7 +17,7 @@ The project uses a global `vuex` state to centrally store the responses to prett There is a global state `loading` (eg: loading.campaigns, loading.lists) that indicates whether an API call for that particular "model" is running. This can be used anywhere in the project to show loading spinners for instance. All the API definitions are in `api/index.js`. It also describes how each API call sets the global `loading` status alongside storing the API responses. -*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually. +*IMPORTANT*: All JSON field names in GET API responses are automatically camel-cased when they're pulled for the sake of consistentcy in the frontend code and for complying with the linter spec in the project (Vue/AirBnB schema). For example, `content_type` becomes `contentType`. When sending responses to the backend, however, they should be snake-cased manually. This is overridden for certain calls such as `/api/config` and `/api/settings` using the `preserveCase: true` param in `api/index.js`. ## Icon pack diff --git a/frontend/public/index.html b/frontend/public/index.html index 8aa84f0..f0801d8 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,7 +7,6 @@ <%= htmlWebpackPlugin.options.title %> -