diff --git a/.env.example b/.env.example index c636fa8..f60c1aa 100644 --- a/.env.example +++ b/.env.example @@ -18,3 +18,7 @@ DB_TIMEZONE=Africa/Nairobi CUSTODIAL_URL_BASE=http://localhost:5003 BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr DATA_URL_BASE=http://localhost:5006 + +#Language +DEFAULT_LANGUAGE=eng +LANGUAGES=eng, swa diff --git a/cmd/africastalking/main.go b/cmd/africastalking/main.go index dfcaca1..fd8b0b0 100644 --- a/cmd/africastalking/main.go +++ b/cmd/africastalking/main.go @@ -14,6 +14,7 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/lang" "git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/initializers" @@ -22,6 +23,7 @@ import ( httpserver "git.grassecon.net/urdt/ussd/internal/http/at" "git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/remote" + "git.grassecon.net/urdt/ussd/internal/args" ) var ( @@ -44,6 +46,8 @@ func main() { var engineDebug bool var host string var port uint + var gettextDir string + var langs args.LangVar flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&database, "db", "gdbm", "database to be used") @@ -51,12 +55,21 @@ func main() { flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") + flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) + ln, err := lang.LanguageFromCode(config.DefaultLanguage) + if err != nil { + fmt.Fprintf(os.Stderr, "default language set error: %v", err) + os.Exit(1) + } + ctx = context.WithValue(ctx, "Language", ln) + pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ diff --git a/cmd/async/main.go b/cmd/async/main.go index bf23d9f..66a4dd0 100644 --- a/cmd/async/main.go +++ b/cmd/async/main.go @@ -12,12 +12,14 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/lang" "git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/remote" + "git.grassecon.net/urdt/ussd/internal/args" ) var ( @@ -54,6 +56,8 @@ func main() { var engineDebug bool var host string var port uint + var gettextDir string + var langs args.LangVar flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") @@ -62,12 +66,22 @@ func main() { flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") + flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) + + ln, err := lang.LanguageFromCode(config.DefaultLanguage) + if err != nil { + fmt.Fprintf(os.Stderr, "default language set error: %v", err) + os.Exit(1) + } + ctx = context.WithValue(ctx, "Language", ln) + pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ diff --git a/cmd/http/main.go b/cmd/http/main.go index 6ddfded..c47447a 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -14,6 +14,7 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/lang" "git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/initializers" @@ -21,6 +22,7 @@ import ( httpserver "git.grassecon.net/urdt/ussd/internal/http" "git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/remote" + "git.grassecon.net/urdt/ussd/internal/args" ) var ( @@ -43,6 +45,8 @@ func main() { var engineDebug bool var host string var port uint + var gettextDir string + var langs args.LangVar flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") flag.StringVar(&database, "db", "gdbm", "database to be used") @@ -50,12 +54,22 @@ func main() { flag.UintVar(&size, "s", 160, "max size of output") flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host") flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port") + flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") + flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) ctx := context.Background() ctx = context.WithValue(ctx, "Database", database) + + ln, err := lang.LanguageFromCode(config.DefaultLanguage) + if err != nil { + fmt.Fprintf(os.Stderr, "default language set error: %v", err) + os.Exit(1) + } + ctx = context.WithValue(ctx, "Language", ln) + pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ diff --git a/cmd/main.go b/cmd/main.go index 4fd084f..a61176d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -10,10 +10,12 @@ import ( "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/lang" "git.grassecon.net/urdt/ussd/config" "git.grassecon.net/urdt/ussd/initializers" "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/internal/args" "git.grassecon.net/urdt/ussd/remote" ) @@ -27,6 +29,7 @@ func init() { initializers.LoadEnvVariables() } +// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility func main() { config.LoadConfig() @@ -35,18 +38,34 @@ func main() { var sessionId string var database string var engineDebug bool + var gettextDir string + var langs args.LangVar flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") flag.StringVar(&database, "db", "gdbm", "database to be used") flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") flag.BoolVar(&engineDebug, "d", false, "use engine debug output") flag.UintVar(&size, "s", 160, "max size of output") + flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory") + flag.Var(&langs, "language", "add symbol resolution for language") flag.Parse() logg.Infof("start command", "dbdir", dbDir, "outputsize", size) + if len(langs.Langs()) == 0 { + langs.Set(config.DefaultLanguage) + } + ctx := context.Background() ctx = context.WithValue(ctx, "SessionId", sessionId) ctx = context.WithValue(ctx, "Database", database) + + ln, err := lang.LanguageFromCode(config.DefaultLanguage) + if err != nil { + fmt.Fprintf(os.Stderr, "default language set error: %v", err) + os.Exit(1) + } + ctx = context.WithValue(ctx, "Language", ln) + pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ @@ -59,8 +78,11 @@ func main() { resourceDir := scriptDir menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir) + if gettextDir != "" { + menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs()) + } - err := menuStorageService.EnsureDbDir() + err = menuStorageService.EnsureDbDir() if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) diff --git a/config/config.go b/config/config.go index 3a8e8ed..02fdaab 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,7 @@ package config import ( "net/url" + "strings" "git.grassecon.net/urdt/ussd/initializers" ) @@ -18,6 +19,11 @@ const ( AliasPrefix = "api/v1/alias" ) +var ( + defaultLanguage = "eng" + languages []string +) + var ( custodialURLBase string dataURLBase string @@ -34,8 +40,28 @@ var ( VoucherTransfersURL string VoucherDataURL string CheckAliasURL string + DefaultLanguage string + Languages []string ) +func setLanguage() error { + defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage) + languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",") + haveDefaultLanguage := false + for i, v := range(languages) { + languages[i] = strings.ReplaceAll(v, " ", "") + if languages[i] == defaultLanguage { + haveDefaultLanguage = true + } + } + + if !haveDefaultLanguage { + languages = append([]string{defaultLanguage}, languages...) + } + + return nil +} + func setBase() error { var err error @@ -60,6 +86,10 @@ func LoadConfig() error { if err != nil { return err } + err = setLanguage() + if err != nil { + return err + } CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath) TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath) BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix) @@ -69,6 +99,8 @@ func LoadConfig() error { VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix) VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix) CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix) + DefaultLanguage = defaultLanguage + Languages = languages return nil } diff --git a/devtools/lang/main.go b/devtools/lang/main.go new file mode 100644 index 0000000..83c68b3 --- /dev/null +++ b/devtools/lang/main.go @@ -0,0 +1,126 @@ +// create language files from environment +package main + +import ( + "flag" + "fmt" + "os" + "path" + "strings" + + "git.defalsify.org/vise.git/logging" + "git.defalsify.org/vise.git/lang" + "git.grassecon.net/urdt/ussd/config" + "git.grassecon.net/urdt/ussd/initializers" +) + +const ( + + changeHeadSrc = `LOAD reset_account_authorized 0 +LOAD reset_incorrect 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH pin_entry flag_account_authorized 0 +` + + selectSrc = `LOAD set_language 6 +RELOAD set_language +CATCH terms flag_account_created 0 +MOVE language_changed +` +) + +var ( + logg = logging.NewVanilla() + mouts string + incmps string +) + +func init() { + initializers.LoadEnvVariables() +} + +func toLanguageLabel(ln lang.Language) string { + s := ln.Name + v := strings.Split(s, " (") + if len(v) > 1 { + s = v[0] + } + return s +} + +func toLanguageKey(ln lang.Language) string { + s := toLanguageLabel(ln) + return strings.ToLower(s) +} + +func main() { + var srcDir string + + flag.StringVar(&srcDir, "o", ".", "resource dir write to") + flag.Parse() + + logg.Infof("start command", "dir", srcDir) + + err := config.LoadConfig() + if err != nil { + fmt.Fprintf(os.Stderr, "config load error: %v", err) + os.Exit(1) + } + logg.Tracef("using languages", "lang", config.Languages) + + for i, v := range(config.Languages) { + ln, err := lang.LanguageFromCode(v) + if err != nil { + fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v) + os.Exit(1) + } + n := i + 1 + s := toLanguageKey(ln) + mouts += fmt.Sprintf("MOUT %s %v\n", s, n) + v = "set_" + ln.Code + incmps += fmt.Sprintf("INCMP %s %v\n", v, n) + + p := path.Join(srcDir, v) + w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err) + os.Exit(1) + } + s = toLanguageLabel(ln) + defer w.Close() + _, err = w.Write([]byte(s)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } + } + src := mouts + "HALT\n" + incmps + src += "INCMP . *\n" + + p := path.Join(srcDir, "select_language.vis") + w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) + os.Exit(1) + } + defer w.Close() + _, err = w.Write([]byte(src)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } + + src = changeHeadSrc + src + p = path.Join(srcDir, "change_language.vis") + w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600) + if err != nil { + fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err) + os.Exit(1) + } + defer w.Close() + _, err = w.Write([]byte(src)) + if err != nil { + fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err) + os.Exit(1) + } +} diff --git a/internal/args/lang.go b/internal/args/lang.go new file mode 100644 index 0000000..f9afdc9 --- /dev/null +++ b/internal/args/lang.go @@ -0,0 +1,34 @@ +package args + +import ( + "strings" + + "git.defalsify.org/vise.git/lang" +) + +type LangVar struct { + v []lang.Language +} + +func(lv *LangVar) Set(s string) error { + v, err := lang.LanguageFromCode(s) + if err != nil { + return err + } + lv.v = append(lv.v, v) + return err +} + +func(lv *LangVar) String() string { + var s []string + for _, v := range(lv.v) { + s = append(s, v.Code) + } + return strings.Join(s, ",") +} + +func(lv *LangVar) Langs() []lang.Language { + return lv.v +} + + diff --git a/internal/storage/storageservice.go b/internal/storage/storageservice.go index 04e75ce..8e6b695 100644 --- a/internal/storage/storageservice.go +++ b/internal/storage/storageservice.go @@ -9,6 +9,7 @@ import ( "git.defalsify.org/vise.git/db" fsdb "git.defalsify.org/vise.git/db/fs" "git.defalsify.org/vise.git/db/postgres" + "git.defalsify.org/vise.git/lang" "git.defalsify.org/vise.git/logging" "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" @@ -30,6 +31,7 @@ type StorageService interface { type MenuStorageService struct { dbDir string resourceDir string + poResource resource.Resource resourceStore db.Db stateStore db.Db userDataStore db.Db @@ -58,6 +60,28 @@ func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService } } +// WithGettext triggers use of gettext for translation of templates and menus. +// +// The first language in `lns` will be used as default language, to resolve node keys to +// language strings. +// +// If `lns` is an empty array, gettext will not be used. +func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService { + if len(lns) == 0 { + logg.Warnf("Gettext requested but no languages supplied") + return ms + } + rs := resource.NewPoResource(lns[0], path) + + for _, ln := range(lns) { + rs = rs.WithLanguage(ln) + } + + ms.poResource = rs + + return ms +} + func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) { database, ok := ctx.Value("Database").(string) if !ok { @@ -120,6 +144,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc return nil, err } rfs := resource.NewDbResource(ms.resourceStore) + if ms.poResource != nil { + logg.InfoCtxf(ctx, "using poresource for menu and template") + rfs.WithMenuGetter(ms.poResource.GetMenu) + rfs.WithTemplateGetter(ms.poResource.GetTemplate) + } return rfs, nil }