diff options
author | 2023-11-17 11:35:28 +0100 | |
---|---|---|
committer | 2023-11-17 11:35:28 +0100 | |
commit | fc02d3c6f7db5a7794448f31fd9d6d81d3d224eb (patch) | |
tree | f792f799abadf784e493933af597d8f2292ab776 /internal/language | |
parent | [bugfix] process account delete side effects in serial, not in parallel (#2360) (diff) | |
download | gotosocial-fc02d3c6f7db5a7794448f31fd9d6d81d3d224eb.tar.xz |
[feature] Set/show instance language(s); show post language on frontend (#2362)
* update go text, include text/display
* [feature] Set instance langs, show post lang on frontend
* go fmt
* WebGet
* set language for whole article, don't use FA icon
* mention instance languages + other optional config vars
* little tweak
* put languages in config properly
* warn log language parse
* change some naming around
* tidy up validate a bit
* lint
* rename LanguageTmpl in template
Diffstat (limited to 'internal/language')
-rw-r--r-- | internal/language/language.go | 184 | ||||
-rw-r--r-- | internal/language/language_test.go | 142 |
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) + } + } +} |