diff options
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) +		} +	} +} | 
