package i18n_test import ( "errors" "fmt" "os" "path/filepath" "strconv" "strings" "sync" "testing" "allaboutapps.dev/aw/go-starter/internal/api" "allaboutapps.dev/aw/go-starter/internal/config" "allaboutapps.dev/aw/go-starter/internal/i18n" "allaboutapps.dev/aw/go-starter/internal/test" "allaboutapps.dev/aw/go-starter/internal/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/text/language" ) func TestServerProvidedI18n(t *testing.T) { test.WithTestServer(t, func(s *api.Server) { // uses /app/web/i18n by default // expect all i18n files were loaded and the defaultLanguage matches the FIRST priority Tag. assert.Equal(t, s.Config.I18n.DefaultLanguage, s.I18n.Tags()[0]) // expect all i18ns were loaded... files, err := os.ReadDir(s.Config.I18n.BundleDirAbs) require.NoError(t, err) msgFilesCount := 0 for _, file := range files { if file.IsDir() || !strings.HasSuffix(file.Name(), ".toml") { continue } msgFilesCount++ } if msgFilesCount == 0 { // no i18n bundles were available, as the defaultLanguage is a tag itself, check for len 1 assert.Len(t, s.I18n.Tags(), 1) } else { assert.Len(t, s.I18n.Tags(), msgFilesCount) } msg := s.I18n.Translate("this.key.will.never.exist", s.Config.I18n.DefaultLanguage) assert.Equal(t, "this.key.will.never.exist", msg) }) } // Note that all following tests use a special message directory within /internal/i18n/testdata. // We do this to ensure we don't depend on your project specific i18n bundle/configuration, // that you would typically store within /web/i18n. func TestI18n(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) assert.Equal(t, language.English, srv.Tags()[0]) assert.Equal(t, language.German, srv.Tags()[1]) assert.Len(t, srv.Tags(), 2) msg := srv.Translate("Test.Welcome", language.German, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Guten Tag Hans", msg) msg = srv.Translate("Test.Welcome", language.English, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) msg = srv.Translate("Test.Welcome", language.Spanish, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) msg = srv.Translate("Test.Welcome", language.English, i18n.Data{"Name": "Franz"}) assert.Equal(t, "Welcome Franz", msg) msg = srv.Translate("Test.Welcome", language.English) assert.Equal(t, "Welcome ", msg) msg = srv.Translate("Test.Body", language.German) assert.Equal(t, "Das ist ein Test", msg) msg = srv.Translate("Test.Body", language.English) assert.Equal(t, "This is a test", msg) msg = srv.Translate("Test.Body", language.Spanish) assert.Equal(t, "This is a test", msg) msg = srv.Translate("Test.Invalid.Key.Does.Not.Exist", language.English) assert.Equal(t, "Test.Invalid.Key.Does.Not.Exist", msg) msg = srv.Translate("Test.Invalid.Key.Does.Not.Exist", language.German) assert.Equal(t, "Test.Invalid.Key.Does.Not.Exist", msg) msg = srv.Translate("Test.String.DE.only", language.English) assert.Equal(t, "Test.String.DE.only", msg) msg = srv.Translate("Test.String.DE.only", language.German) assert.Equal(t, "This key only exists in DE", msg) msg, err = srv.TranslateMaybe("Test.String.DE.only", language.English) require.Error(t, err) assert.Empty(t, msg) // no fallback! msg = srv.Translate("Test.String.EN.only", language.English) assert.Equal(t, "This key only exists in EN", msg) msg, err = srv.TranslateMaybe("Test.String.EN.only", language.German) require.Error(t, err) assert.Equal(t, "This key only exists in EN", msg) // fallback (but error) msg = srv.Translate("Test.String.EN.only", language.German) assert.Equal(t, "Test.String.EN.only", msg) msg = srv.Translate("", language.German) // empty key assert.Empty(t, msg) msg, err = srv.TranslateMaybe("", language.German) // empty key require.Error(t, err) assert.Empty(t, msg) // ensure language subvariants are supported deAt := srv.ParseLang("de-AT") assert.NotEqual(t, language.German, deAt) msg = srv.Translate("Test.Body", deAt) assert.Equal(t, "Das ist ein Test", msg) } func TestI18nConcurrentUsage(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) wg := sync.WaitGroup{} wg.Add(100) for i := 0; i < 100; i++ { go func(index int) { msg := srv.Translate("Test.Welcome", language.German, i18n.Data{"Name": strconv.Itoa(index)}) assert.Equal(t, fmt.Sprintf("Guten Tag %v", index), msg) msg = srv.Translate("Test.Welcome", language.English, i18n.Data{"Name": strconv.Itoa(index)}) assert.Equal(t, fmt.Sprintf("Welcome %v", index), msg) msg = srv.Translate("Test.Welcome", language.Spanish, i18n.Data{"Name": strconv.Itoa(index)}) assert.Equal(t, fmt.Sprintf("Welcome %v", index), msg) msg = srv.Translate("Test.Welcome", language.English, i18n.Data{"Name": "Franz"}) assert.Equal(t, "Welcome Franz", msg) msg = srv.Translate("Test.Welcome", language.English) assert.Equal(t, "Welcome ", msg) msg = srv.Translate("Test.Body", language.German) assert.Equal(t, "Das ist ein Test", msg) msg = srv.Translate("Test.Body", language.English) assert.Equal(t, "This is a test", msg) msg = srv.Translate("Test.Body", language.Spanish) assert.Equal(t, "This is a test", msg) wg.Done() }(i) } wg.Wait() } func TestI18nOtherDefault(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.German, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) assert.Equal(t, language.German, srv.Tags()[0]) assert.Equal(t, language.English, srv.Tags()[1]) assert.Len(t, srv.Tags(), 2) } func TestI18nInexistantDefault(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.Italian, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) assert.Equal(t, language.Italian, srv.Tags()[0]) assert.Equal(t, language.German, srv.Tags()[1]) assert.Equal(t, language.English, srv.Tags()[2]) assert.Len(t, srv.Tags(), 3) } func TestI18nEmpty(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.Italian, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-empty"), }) require.NoError(t, err) assert.Len(t, srv.Tags(), 1) // the DefaultLanguage should still be set! assert.Equal(t, language.Italian, srv.Tags()[0]) tag := srv.ParseAcceptLanguage("de,en-US;q=0.7,en;q=0.3") assert.Equal(t, language.Italian, tag) msg := srv.Translate("no.test.key.exists", tag) assert.Equal(t, "no.test.key.exists", msg) msg, err = srv.TranslateMaybe("no.test.key.exists", tag) require.Error(t, err) assert.Empty(t, msg) msg = srv.Translate("no.test.key.exists", language.Ukrainian) assert.Equal(t, "no.test.key.exists", msg) msg, err = srv.TranslateMaybe("no.test.key.exists", language.Ukrainian) require.Error(t, err) assert.Empty(t, msg) } func TestI18nSpecialized(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.AmericanEnglish, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-specialized"), }) require.NoError(t, err) assert.Len(t, srv.Tags(), 4) // specialized subvariant is default assert.Equal(t, language.AmericanEnglish, srv.Tags()[0]) msg := srv.Translate("test.punchline", language.AmericanEnglish) assert.Equal(t, "I can has HUMOR?", msg) msg = srv.Translate("test.punchline", language.BritishEnglish) assert.Equal(t, "I can has HUMOUR?", msg) msg = srv.Translate("test.punchline", language.English) assert.Equal(t, "I can has HUMOR?", msg) // fall back to default msg = srv.Translate("test.punchline", language.German) assert.Equal(t, "Habe ich Humor?", msg) // jump to parsed Austrian German tag := srv.ParseAcceptLanguage("de-at,en-US;q=0.7,en;q=0.3") // explicit Austrian tag msg = srv.Translate("test.punchline", tag) assert.Equal(t, "Koan i Humor?", msg) // jump to parsed Austrian German } func TestReservedKeywordsResolve(t *testing.T) { // "reserved" keys: // "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other" // see https://github.com/nicksnyder/go-i18n/blob/2180cd9f35b3e125cfe3773a6bf3ea483347f060/v2/i18n/message.go#L181 srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-reserved"), }) require.NoError(t, err) assert.Len(t, srv.Tags(), 2) // single reserved word (not yet mapped, requires 2 keywords at least) msg := srv.Translate("reserved1.zero", language.English) assert.Equal(t, "Zero", msg) msg = srv.Translate("reserved2.one", language.English) assert.Equal(t, "One", msg) msg = srv.Translate("reserved3.two", language.English) assert.Equal(t, "Two", msg) msg = srv.Translate("reserved4.few", language.English) assert.Equal(t, "Few", msg) msg = srv.Translate("reserved5.many", language.English) assert.Equal(t, "Many", msg) msg = srv.Translate("reserved6.other", language.English) assert.Equal(t, "Other", msg) msg = srv.Translate("reserved7.id", language.English) assert.Equal(t, "id", msg) msg = srv.Translate("reserved8.description", language.English) assert.Equal(t, "Description", msg) // single parent is not directly resolveable msg = srv.Translate("reserved2", language.English) assert.Equal(t, "reserved2", msg) // german: single reserved word msg = srv.Translate("reserved1.zero", language.German) assert.Equal(t, "Null", msg) msg = srv.Translate("reserved2.one", language.German) assert.Equal(t, "Eins", msg) msg = srv.Translate("reserved3.two", language.German) assert.Equal(t, "Zwei", msg) msg = srv.Translate("reserved4.few", language.German) assert.Equal(t, "Wenig", msg) msg = srv.Translate("reserved5.many", language.German) assert.Equal(t, "Mehr", msg) msg = srv.Translate("reserved6.other", language.German) assert.Equal(t, "Andere", msg) msg = srv.Translate("reserved7.id", language.German) assert.Equal(t, "ID", msg) msg = srv.Translate("reserved8.description", language.German) assert.Equal(t, "Beschreibung", msg) // Combined toml map: all reserved words // This does not work as it's parsed as map and CLDR plural rules now apply! // The last key is interpreted as https://cldr.unicode.org/index/cldr-spec/plural-rules msg = srv.Translate("reservedMap.zero", language.English) assert.Equal(t, "reservedMap.zero", msg) msg = srv.Translate("reservedMap.one", language.English) assert.Equal(t, "reservedMap.one", msg) msg = srv.Translate("reservedMap.two", language.English) assert.Equal(t, "reservedMap.two", msg) msg = srv.Translate("reservedMap.few", language.English) assert.Equal(t, "reservedMap.few", msg) msg = srv.Translate("reservedMap.many", language.English) assert.Equal(t, "reservedMap.many", msg) msg = srv.Translate("reservedMap.other", language.English) assert.Equal(t, "reservedMap.other", msg) msg = srv.TranslatePlural("reservedMap", 0, language.English) assert.Equal(t, "Other", msg) msg = srv.TranslatePlural("reservedMap", 1, language.English) assert.Equal(t, "One", msg) msg = srv.TranslatePlural("reservedMap", 2, language.English) assert.Equal(t, "Other", msg) msg = srv.TranslatePlural("reservedMap", "asdfasdf", language.English) assert.Equal(t, "reservedMap (count=asdfasdf)", msg) // plain toml: all reserved words // This does not work as it's parsed as map and CLDR plural rules now apply! // The last key is interpreted as https://cldr.unicode.org/index/cldr-spec/plural-rules msg = srv.Translate("reserved.plain.zero", language.English) assert.Equal(t, "reserved.plain.zero", msg) msg = srv.Translate("reserved.plain.one", language.English) assert.Equal(t, "reserved.plain.one", msg) msg = srv.Translate("reserved.plain.two", language.English) assert.Equal(t, "reserved.plain.two", msg) msg = srv.Translate("reserved.plain.few", language.English) assert.Equal(t, "reserved.plain.few", msg) msg = srv.Translate("reserved.plain.many", language.English) assert.Equal(t, "reserved.plain.many", msg) msg = srv.Translate("reserved.plain.other", language.English) assert.Equal(t, "reserved.plain.other", msg) msg = srv.Translate("reserved.plain2.id", language.English) assert.Equal(t, "reserved.plain2.id", msg) msg = srv.Translate("reserved.plain2.description", language.English) assert.Equal(t, "reserved.plain2.description", msg) msg = srv.Translate("reserved.plain3.id", language.English) assert.Equal(t, "reserved.plain3.id", msg) msg = srv.Translate("reserved.plain3.description", language.English) assert.Equal(t, "reserved.plain3.description", msg) msg = srv.Translate("reserved.plain3.test", language.English) assert.Equal(t, "reserved.plain3.test", msg) } func TestI18nPlural(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-plural"), }) require.NoError(t, err) assert.Len(t, srv.Tags(), 2) msg := srv.TranslatePlural("cats", 0, language.AmericanEnglish) assert.Equal(t, "I've 0 cats.", msg) // zero is not supported for CLDR English, "I don't have a cat." is not possible! msg = srv.TranslatePlural("cats", 1, language.BritishEnglish) assert.Equal(t, "I've one cat.", msg) msg = srv.TranslatePlural("cats", 2, language.English) assert.Equal(t, "I've 2 cats.", msg) msg = srv.TranslatePlural("cats", 8, language.English) assert.Equal(t, "I've 8 cats.", msg) msg = srv.TranslatePlural("cats", -1, language.English) // negative and positive scales behave the same! assert.Equal(t, "I've one cat.", msg) msg = srv.TranslatePlural("cats", -2, language.English) // negative and positive scales behave the same! assert.Equal(t, "I've -2 cats.", msg) msg, err = srv.TranslatePluralMaybe("cats", -2, language.English) require.NoError(t, err) assert.Equal(t, "I've -2 cats.", msg) msg = srv.TranslatePlural("cats", nil, language.English) assert.Equal(t, "I've cats.", msg) // invalid count msg, err = srv.TranslatePluralMaybe("cats", nil, language.English) require.NoError(t, err) assert.Equal(t, "I've cats.", msg) // invalid count msg = srv.TranslatePlural("cats", "many", language.English) assert.Equal(t, "cats (count=many)", msg) // internal failed to translate plural! // overwrite Count msg = srv.TranslatePlural("cats", 8, language.English, i18n.Data{"Count": "too many"}) assert.Equal(t, "I've too many cats.", msg) msg = srv.TranslatePlural("cats", 0, language.German) assert.Equal(t, "Ich habe 0 Katzen.", msg) // zero is not supported for CLDR German, "Ich habe keine Katze." is not possible! msg = srv.TranslatePlural("cats", 1, language.German) assert.Equal(t, "Ich habe eine Katze.", msg) msg = srv.TranslatePlural("cats", 2, language.German) assert.Equal(t, "Ich habe 2 Katzen.", msg) msg = srv.TranslatePlural("cats", 8, language.German) assert.Equal(t, "Ich habe 8 Katzen.", msg) msg = srv.TranslatePlural("cats", "viele", language.German) assert.Equal(t, "cats (count=viele)", msg) // internal failed to translate plural! msg, err = srv.TranslatePluralMaybe("cats", "viele", language.German) require.Error(t, err) assert.Empty(t, msg) // empty string for errors! // overwrite Count msg = srv.TranslatePlural("cats", 8, language.German, i18n.Data{"Count": "zu viele"}) assert.Equal(t, "Ich habe zu viele Katzen.", msg) // unknown language string tag := srv.ParseLang("xx") assert.Equal(t, language.English, tag) msg = srv.TranslatePlural("cats", 8, tag) assert.Equal(t, "I've 8 cats.", msg) // fall back to English // invalid specialized language string tag = srv.ParseLang("de-xx") msg = srv.TranslatePlural("cats", 8, tag) assert.Equal(t, "Ich habe 8 Katzen.", msg) // fall back to German // invalid language string tag = srv.ParseLang("§$%/%&/(/&%/)(") assert.Equal(t, language.English, tag) msg = srv.TranslatePlural("cats", 8, tag) assert.Equal(t, "I've 8 cats.", msg) // fall back to English // unknown msg = srv.TranslatePlural("this.key.will.never.exist", nil, language.English) assert.Equal(t, "this.key.will.never.exist (count=)", msg) msg, err = srv.TranslatePluralMaybe("this.key.will.never.exist", nil, language.English) require.Error(t, err) assert.Empty(t, msg) // empty string for errors! } func TestI18nUndetermined(t *testing.T) { _, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-undetermined"), }) require.Error(t, err) unwrappedErr := errors.Unwrap(err) require.Error(t, unwrappedErr) assert.Equal(t, unwrappedErr, errors.New("undetermined language at index 1 in i18n message bundle: [en und]")) } func TestI18nUndeterminedDefaultLanguage(t *testing.T) { _, err := i18n.New(config.I18n{ DefaultLanguage: language.Und, // Undetermined is disallowed BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.Error(t, err) unwrappedErr := errors.Unwrap(err) require.Error(t, unwrappedErr) assert.Equal(t, unwrappedErr, errors.New("undetermined language at index 0 in i18n message bundle: [und de en]")) } func TestI18nInvalidToml(t *testing.T) { _, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n-invalid"), }) require.Error(t, err) } func TestI18nInexistantFolder(t *testing.T) { _, err := i18n.New(config.I18n{ DefaultLanguage: language.AmericanEnglish, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n/this/folder/does/not/exist"), }) require.Error(t, err) } func TestParseAcceptLanguage(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) tag := srv.ParseAcceptLanguage("de,en-US;q=0.7,en;q=0.3") assert.Equal(t, language.German, tag) tag = srv.ParseAcceptLanguage("de-AT,en-US;q=0.7,en;q=0.3") assert.NotEqual(t, language.German, tag) // actual: de-u-rg-atzzzz // unknown language header tag = srv.ParseAcceptLanguage("xx,en-US;q=0.7,en;q=0.3") assert.Equal(t, language.English, tag) msg := srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) // invalid specialized language string tag = srv.ParseAcceptLanguage("de-xx,en-US;q=0.7,en;q=0.3") msg = srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Guten Tag Hans", msg) // invalid language header tag = srv.ParseAcceptLanguage("§$%/%&/(/&%/)(") assert.Equal(t, language.English, tag) msg = srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) } func TestParseLanguage(t *testing.T) { srv, err := i18n.New(config.I18n{ DefaultLanguage: language.English, BundleDirAbs: filepath.Join(util.GetProjectRootDir(), "/internal/i18n/testdata/i18n"), }) require.NoError(t, err) tag := srv.ParseLang("de") assert.Equal(t, language.German, tag) // unknown language string tag = srv.ParseLang("xx") assert.Equal(t, language.English, tag) msg := srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) // invalid specialized language string tag = srv.ParseLang("de-xx") msg = srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Guten Tag Hans", msg) // fall back to German // invalid language string tag = srv.ParseLang("§$%/%&/(/&%/)(") assert.Equal(t, language.English, tag) msg = srv.Translate("Test.Welcome", tag, i18n.Data{"Name": "Hans"}) assert.Equal(t, "Welcome Hans", msg) }