summaryrefslogtreecommitdiff
path: root/internal/language
diff options
context:
space:
mode:
Diffstat (limited to 'internal/language')
-rw-r--r--internal/language/language.go184
-rw-r--r--internal/language/language_test.go142
2 files changed, 326 insertions, 0 deletions
diff --git a/internal/language/language.go b/internal/language/language.go
new file mode 100644
index 000000000..d91e3a4db
--- /dev/null
+++ b/internal/language/language.go
@@ -0,0 +1,184 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package language
+
+import (
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "golang.org/x/text/language"
+ "golang.org/x/text/language/display"
+)
+
+var namer display.Namer
+
+// InitLangs parses languages from the
+// given slice of tags, and sets the `namer`
+// display.Namer for the instance.
+//
+// This function should only be called once,
+// since setting the namer is not thread safe.
+func InitLangs(tagStrs []string) (Languages, error) {
+ var (
+ languages = make(Languages, len(tagStrs))
+ tags = make([]language.Tag, len(tagStrs))
+ )
+
+ // Reset namer.
+ namer = nil
+
+ // Parse all tags first.
+ for i, tagStr := range tagStrs {
+ tag, err := language.Parse(tagStr)
+ if err != nil {
+ return nil, gtserror.Newf(
+ "error parsing %s as BCP47 language tag: %w",
+ tagStr, err,
+ )
+ }
+ tags[i] = tag
+ }
+
+ // Check if we can set a namer.
+ if len(tags) != 0 {
+ namer = display.Languages(tags[0])
+ }
+
+ // Fall namer back to English.
+ if namer == nil {
+ namer = display.Languages(language.English)
+ }
+
+ // Parse nice language models from tags
+ // (this will use the namer we just set).
+ for i, tag := range tags {
+ languages[i] = ParseTag(tag)
+ }
+
+ return languages, nil
+}
+
+// Language models a BCP47 language tag
+// along with helper strings for the tag.
+type Language struct {
+ // BCP47 language tag
+ Tag language.Tag
+ // Normalized string
+ // of BCP47 tag.
+ TagStr string
+ // Human-readable
+ // language name(s).
+ DisplayStr string
+}
+
+// MarshalText implements encoding.TextMarshaler{}.
+func (l *Language) MarshalText() ([]byte, error) {
+ return []byte(l.TagStr), nil
+}
+
+// UnmarshalText implements encoding.TextUnmarshaler{}.
+func (l *Language) UnmarshalText(text []byte) error {
+ lang, err := Parse(string(text))
+ if err != nil {
+ return err
+ }
+
+ *l = *lang
+ return nil
+}
+
+type Languages []*Language
+
+func (l Languages) Tags() []language.Tag {
+ tags := make([]language.Tag, len(l))
+ for i, lang := range l {
+ tags[i] = lang.Tag
+ }
+
+ return tags
+}
+
+func (l Languages) TagStrs() []string {
+ tagStrs := make([]string, len(l))
+ for i, lang := range l {
+ tagStrs[i] = lang.TagStr
+ }
+
+ return tagStrs
+}
+
+func (l Languages) DisplayStrs() []string {
+ displayStrs := make([]string, len(l))
+ for i, lang := range l {
+ displayStrs[i] = lang.DisplayStr
+ }
+
+ return displayStrs
+}
+
+// ParseTag parses and nicely formats the input language BCP47 tag,
+// returning a Language with ready-to-use display and tag strings.
+func ParseTag(tag language.Tag) *Language {
+ l := new(Language)
+ l.Tag = tag
+ l.TagStr = tag.String()
+
+ var (
+ // Our name for the language.
+ name string
+ // Language's name for itself.
+ selfName = display.Self.Name(tag)
+ )
+
+ // Try to use namer
+ // (if initialized).
+ if namer != nil {
+ name = namer.Name(tag)
+ }
+
+ switch {
+ case name == "":
+ // We don't have a name for
+ // this language, just use
+ // its own name for itself.
+ l.DisplayStr = selfName
+
+ case name == selfName:
+ // Avoid repeating ourselves:
+ // showing "English (English)"
+ // is not useful.
+ l.DisplayStr = name
+
+ default:
+ // Include our name for the
+ // language, and its own
+ // name for itself.
+ l.DisplayStr = name + " " + "(" + selfName + ")"
+ }
+
+ return l
+}
+
+// Parse parses and nicely formats the input language BCP47 tag,
+// returning a Language with ready-to-use display and tag strings.
+func Parse(lang string) (*Language, error) {
+ tag, err := language.Parse(lang)
+ if err != nil {
+ return nil, err
+ }
+
+ return ParseTag(tag), nil
+}
diff --git a/internal/language/language_test.go b/internal/language/language_test.go
new file mode 100644
index 000000000..024448ab4
--- /dev/null
+++ b/internal/language/language_test.go
@@ -0,0 +1,142 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package language_test
+
+import (
+ "slices"
+ "testing"
+
+ "github.com/superseriousbusiness/gotosocial/internal/language"
+ golanguage "golang.org/x/text/language"
+)
+
+func TestInstanceLangs(t *testing.T) {
+ for i, test := range []struct {
+ InstanceLangs []string
+ expectedLangs []golanguage.Tag
+ expectedLangStrs []string
+ expectedErr error
+ parseDisplayLang string
+ expectedDisplayLang string
+ }{
+ {
+ InstanceLangs: []string{"en-us", "fr"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.AmericanEnglish,
+ golanguage.French,
+ },
+ expectedLangStrs: []string{
+ "American English",
+ "French (français)",
+ },
+ parseDisplayLang: "de",
+ expectedDisplayLang: "German (Deutsch)",
+ },
+ {
+ InstanceLangs: []string{"fr", "en-us"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.French,
+ golanguage.AmericanEnglish,
+ },
+ expectedLangStrs: []string{
+ "français",
+ "anglais américain (American English)",
+ },
+ parseDisplayLang: "de",
+ expectedDisplayLang: "allemand (Deutsch)",
+ },
+ {
+ InstanceLangs: []string{},
+ expectedLangs: []golanguage.Tag{},
+ expectedLangStrs: []string{},
+ parseDisplayLang: "de",
+ expectedDisplayLang: "German (Deutsch)",
+ },
+ {
+ InstanceLangs: []string{"zh"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.Chinese,
+ },
+ expectedLangStrs: []string{
+ "中文",
+ },
+ parseDisplayLang: "de",
+ expectedDisplayLang: "德语 (Deutsch)",
+ },
+ {
+ InstanceLangs: []string{"ar", "en"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.Arabic,
+ golanguage.English,
+ },
+ expectedLangStrs: []string{
+ "العربية",
+ "الإنجليزية (English)",
+ },
+ parseDisplayLang: "fi",
+ expectedDisplayLang: "الفنلندية (suomi)",
+ },
+ {
+ InstanceLangs: []string{"en-us"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.AmericanEnglish,
+ },
+ expectedLangStrs: []string{
+ "American English",
+ },
+ parseDisplayLang: "en-us",
+ expectedDisplayLang: "American English",
+ },
+ {
+ InstanceLangs: []string{"en-us"},
+ expectedLangs: []golanguage.Tag{
+ golanguage.AmericanEnglish,
+ },
+ expectedLangStrs: []string{
+ "American English",
+ },
+ parseDisplayLang: "en-gb",
+ expectedDisplayLang: "British English",
+ },
+ } {
+ languages, err := language.InitLangs(test.InstanceLangs)
+ if err != test.expectedErr {
+ t.Errorf("test %d expected error %v, got %v", i, test.expectedErr, err)
+ }
+
+ parsedTags := languages.Tags()
+ if !slices.Equal(test.expectedLangs, parsedTags) {
+ t.Errorf("test %d expected language tags %v, got %v", i, test.expectedLangs, parsedTags)
+ }
+
+ parsedLangStrs := languages.DisplayStrs()
+ if !slices.Equal(test.expectedLangStrs, parsedLangStrs) {
+ t.Errorf("test %d expected language strings %v, got %v", i, test.expectedLangStrs, parsedLangStrs)
+ }
+
+ parsedLang, err := language.Parse(test.parseDisplayLang)
+ if err != nil {
+ t.Errorf("unexpected error %v", err)
+ return
+ }
+
+ if test.expectedDisplayLang != parsedLang.DisplayStr {
+ t.Errorf("test %d expected to parse language %v, got %v", i, test.expectedDisplayLang, parsedLang.DisplayStr)
+ }
+ }
+}