Add support for loading external i18n language files.

The new `--i18n-dir` directory allows the loading of an external
directory of i18n JSON files, milar to have `--static-dir`
works. New languages can be added and existing language files
can be customized this way.

This commit changes file loading behaviour so that invalid or
non-existent don't halt the execution of the app completely but
merely throw a warning and continue with the default (en) lang.
This commit is contained in:
Kailash Nadh 2021-04-17 14:26:56 +05:30
parent 4ddaba889f
commit c479a90c42
4 changed files with 34 additions and 14 deletions

View File

@ -29,8 +29,8 @@ func handleGetI18nLang(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.") return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
} }
i, err := getI18nLang(lang, app.fs) i, ok, err := getI18nLang(lang, app.fs)
if err != nil { if err != nil && !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.") return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
} }
@ -65,29 +65,31 @@ func getI18nLangList(lang string, app *App) ([]i18nLang, error) {
return out, nil return out, nil
} }
func getI18nLang(lang string, fs stuffbin.FileSystem) (*i18n.I18n, error) { // The bool indicates whether the specified language could be loaded. If it couldn't
// be, the app shouldn't halt but throw a warning.
func getI18nLang(lang string, fs stuffbin.FileSystem) (*i18n.I18n, bool, error) {
const def = "en" const def = "en"
b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def)) b, err := fs.Read(fmt.Sprintf("/i18n/%s.json", def))
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading default i18n language file: %s: %v", def, err) return nil, false, fmt.Errorf("error reading default i18n language file: %s: %v", def, err)
} }
// Initialize with the default language. // Initialize with the default language.
i, err := i18n.New(b) i, err := i18n.New(b)
if err != nil { if err != nil {
return nil, fmt.Errorf("error unmarshalling i18n language: %v", err) return nil, false, fmt.Errorf("error unmarshalling i18n language: %s: %v", lang, err)
} }
// Load the selected language on top of it. // Load the selected language on top of it.
b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang)) b, err = fs.Read(fmt.Sprintf("/i18n/%s.json", lang))
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading i18n language file: %v", err) return i, true, fmt.Errorf("error reading i18n language file: %s: %v", lang, err)
} }
if err := i.Load(b); err != nil { if err := i.Load(b); err != nil {
return nil, fmt.Errorf("error loading i18n language file: %v", err) return i, true, fmt.Errorf("error loading i18n language file: %s: %v", lang, err)
} }
return i, nil return i, true, nil
} }

View File

@ -82,6 +82,7 @@ func initFlags() {
f.Bool("version", false, "current version of the build") f.Bool("version", false, "current version of the build")
f.Bool("new-config", false, "generate sample config file") f.Bool("new-config", false, "generate sample config file")
f.String("static-dir", "", "(optional) path to directory with static files") f.String("static-dir", "", "(optional) path to directory with static files")
f.String("i18n-dir", "", "(optional) path to directory with i18n language files")
f.Bool("yes", false, "assume 'yes' to prompts, eg: during --install") f.Bool("yes", false, "assume 'yes' to prompts, eg: during --install")
if err := f.Parse(os.Args[1:]); err != nil { if err := f.Parse(os.Args[1:]); err != nil {
lo.Fatalf("error loading flags: %v", err) lo.Fatalf("error loading flags: %v", err)
@ -107,7 +108,7 @@ func initConfigFiles(files []string, ko *koanf.Koanf) {
// initFileSystem initializes the stuffbin FileSystem to provide // initFileSystem initializes the stuffbin FileSystem to provide
// access to bunded static assets to the app. // access to bunded static assets to the app.
func initFS(staticDir string) stuffbin.FileSystem { func initFS(staticDir, i18nDir string) stuffbin.FileSystem {
// Get the executable's path. // Get the executable's path.
path, err := os.Executable() path, err := os.Executable()
if err != nil { if err != nil {
@ -144,7 +145,7 @@ func initFS(staticDir string) stuffbin.FileSystem {
} }
} }
// Optional static directory to override files. // Optional static directory to override static files.
if staticDir != "" { if staticDir != "" {
lo.Printf("loading static files from: %v", staticDir) lo.Printf("loading static files from: %v", staticDir)
fStatic, err := stuffbin.NewLocalFS("/", []string{ fStatic, err := stuffbin.NewLocalFS("/", []string{
@ -161,6 +162,19 @@ func initFS(staticDir string) stuffbin.FileSystem {
lo.Fatalf("error merging static directory: %s: %v", staticDir, err) lo.Fatalf("error merging static directory: %s: %v", staticDir, err)
} }
} }
// Optional static directory to override i18n language files.
if i18nDir != "" {
lo.Printf("loading i18n language files from: %v", i18nDir)
fi18n, err := stuffbin.NewLocalFS("/", []string{i18nDir + ":/i18n"}...)
if err != nil {
lo.Fatalf("failed reading i18n directory: %s: %v", i18nDir, err)
}
if err := fs.Merge(fi18n); err != nil {
lo.Fatalf("error merging i18n directory: %s: %v", i18nDir, err)
}
}
return fs return fs
} }
@ -262,10 +276,14 @@ func initConstants() *constants {
// and then the selected language is loaded on top of it so that if there are // 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. // missing translations in it, the default English translations show up.
func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n { func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
i, err := getI18nLang(lang, fs) i, ok, err := getI18nLang(lang, fs)
if err != nil { if err != nil {
if ok {
lo.Println(err)
} else {
lo.Fatal(err) lo.Fatal(err)
} }
}
return i return i
} }

View File

@ -166,7 +166,7 @@ func newConfigFile() error {
// Initialize the static file system into which all // Initialize the static file system into which all
// required static assets (.sql, .js files etc.) are loaded. // required static assets (.sql, .js files etc.) are loaded.
fs := initFS("") fs := initFS("", "")
b, err := fs.Read("config.toml.sample") b, err := fs.Read("config.toml.sample")
if err != nil { if err != nil {
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err) return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)

View File

@ -106,7 +106,7 @@ func init() {
// Connect to the database, load the filesystem to read SQL queries. // Connect to the database, load the filesystem to read SQL queries.
db = initDB() db = initDB()
fs = initFS(ko.String("static-dir")) fs = initFS(ko.String("static-dir"), ko.String("i18n-dir"))
// Installer mode? This runs before the SQL queries are loaded and prepared // Installer mode? This runs before the SQL queries are loaded and prepared
// as the installer needs to work on an empty DB. // as the installer needs to work on an empty DB.