diff options
author | Frédéric Guillot <fred@miniflux.net> | 2017-11-19 21:10:04 -0800 |
---|---|---|
committer | Frédéric Guillot <fred@miniflux.net> | 2017-11-19 22:01:46 -0800 |
commit | 8ffb773f43c8dc54801ca1d111854e7e881c93c9 (patch) | |
tree | 38133a2fc612597a75fed1d13e5b4042f58a2b7e /locale |
First commit
Diffstat (limited to 'locale')
-rw-r--r-- | locale/language.go | 47 | ||||
-rw-r--r-- | locale/locale.go | 30 | ||||
-rw-r--r-- | locale/locale_test.go | 103 | ||||
-rw-r--r-- | locale/plurals.go | 101 | ||||
-rw-r--r-- | locale/translations.go | 136 | ||||
-rw-r--r-- | locale/translations/en_US.json | 10 | ||||
-rw-r--r-- | locale/translations/fr_FR.json | 113 | ||||
-rw-r--r-- | locale/translator.go | 40 |
8 files changed, 580 insertions, 0 deletions
diff --git a/locale/language.go b/locale/language.go new file mode 100644 index 0000000..c3deda3 --- /dev/null +++ b/locale/language.go @@ -0,0 +1,47 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package locale + +import "fmt" + +type Language struct { + language string + translations Translation +} + +func (l *Language) Get(key string, args ...interface{}) string { + var translation string + + str, found := l.translations[key] + if !found { + translation = key + } else { + translation = str.(string) + } + + return fmt.Sprintf(translation, args...) +} + +func (l *Language) Plural(key string, n int, args ...interface{}) string { + translation := key + slices, found := l.translations[key] + if found { + + pluralForm, found := pluralForms[l.language] + if !found { + pluralForm = pluralForms["default"] + } + + index := pluralForm(n) + translations := slices.([]interface{}) + translation = key + + if len(translations) > index { + translation = translations[index].(string) + } + } + + return fmt.Sprintf(translation, args...) +} diff --git a/locale/locale.go b/locale/locale.go new file mode 100644 index 0000000..4900525 --- /dev/null +++ b/locale/locale.go @@ -0,0 +1,30 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package locale + +import "log" + +type Translation map[string]interface{} + +type Locales map[string]Translation + +func Load() *Translator { + translator := NewTranslator() + + for language, translations := range Translations { + log.Println("Loading translation:", language) + translator.AddLanguage(language, translations) + } + + return translator +} + +// GetAvailableLanguages returns the list of available languages. +func GetAvailableLanguages() map[string]string { + return map[string]string{ + "en_US": "English", + "fr_FR": "Français", + } +} diff --git a/locale/locale_test.go b/locale/locale_test.go new file mode 100644 index 0000000..baddd1e --- /dev/null +++ b/locale/locale_test.go @@ -0,0 +1,103 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. +package locale + +import "testing" + +func TestTranslateWithMissingLanguage(t *testing.T) { + translator := NewTranslator() + translation := translator.GetLanguage("en_US").Get("auth.username") + + if translation != "auth.username" { + t.Errorf("Wrong translation, got %s", translation) + } +} + +func TestTranslateWithExistingKey(t *testing.T) { + data := `{"auth.username": "Username"}` + translator := NewTranslator() + translator.AddLanguage("en_US", data) + translation := translator.GetLanguage("en_US").Get("auth.username") + + if translation != "Username" { + t.Errorf("Wrong translation, got %s", translation) + } +} + +func TestTranslateWithMissingKey(t *testing.T) { + data := `{"auth.username": "Username"}` + translator := NewTranslator() + translator.AddLanguage("en_US", data) + translation := translator.GetLanguage("en_US").Get("auth.password") + + if translation != "auth.password" { + t.Errorf("Wrong translation, got %s", translation) + } +} + +func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) { + translator := NewTranslator() + translator.AddLanguage("fr_FR", "") + translation := translator.GetLanguage("fr_FR").Get("Status: %s", "ok") + + if translation != "Status: ok" { + t.Errorf("Wrong translation, got %s", translation) + } +} + +func TestTranslatePluralWithDefaultRule(t *testing.T) { + data := `{"number_of_users": ["Il y a %d utilisateur (%s)", "Il y a %d utilisateurs (%s)"]}` + translator := NewTranslator() + translator.AddLanguage("fr_FR", data) + language := translator.GetLanguage("fr_FR") + + translation := language.Plural("number_of_users", 1, 1, "some text") + expected := "Il y a 1 utilisateur (some text)" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } + + translation = language.Plural("number_of_users", 2, 2, "some text") + expected = "Il y a 2 utilisateurs (some text)" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } +} + +func TestTranslatePluralWithRussianRule(t *testing.T) { + data := `{"key": ["из %d книги за %d день", "из %d книг за %d дня", "из %d книг за %d дней"]}` + translator := NewTranslator() + translator.AddLanguage("ru_RU", data) + language := translator.GetLanguage("ru_RU") + + translation := language.Plural("key", 1, 1, 1) + expected := "из 1 книги за 1 день" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } + + translation = language.Plural("key", 2, 2, 2) + expected = "из 2 книг за 2 дня" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } + + translation = language.Plural("key", 5, 5, 5) + expected = "из 5 книг за 5 дней" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } +} + +func TestTranslatePluralWithMissingTranslation(t *testing.T) { + translator := NewTranslator() + translator.AddLanguage("fr_FR", "") + language := translator.GetLanguage("fr_FR") + + translation := language.Plural("number_of_users", 2) + expected := "number_of_users" + if translation != expected { + t.Errorf(`Wrong translation, got "%s" instead of "%s"`, translation, expected) + } +} diff --git a/locale/plurals.go b/locale/plurals.go new file mode 100644 index 0000000..d94f238 --- /dev/null +++ b/locale/plurals.go @@ -0,0 +1,101 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package locale + +// See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html +// And http://www.unicode.org/cldr/charts/29/supplemental/language_plural_rules.html +var pluralForms = map[string]func(n int) int{ + // nplurals=2; plural=(n != 1); + "default": func(n int) int { + if n != 1 { + return 1 + } + + return 0 + }, + // nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5); + "ar_AR": func(n int) int { + if n == 0 { + return 0 + } + + if n == 1 { + return 1 + } + + if n == 2 { + return 2 + } + + if n%100 >= 3 && n%100 <= 10 { + return 3 + } + + if n%100 >= 11 { + return 4 + } + + return 5 + }, + // nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; + "cs_CZ": func(n int) int { + if n == 1 { + return 0 + } + + if n >= 2 && n <= 4 { + return 1 + } + + return 2 + }, + // nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); + "pl_PL": func(n int) int { + if n == 1 { + return 0 + } + + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return 1 + } + + return 2 + }, + // nplurals=2; plural=(n > 1); + "pt_BR": func(n int) int { + if n > 1 { + return 1 + } + return 0 + }, + // nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); + "ru_RU": func(n int) int { + if n%10 == 1 && n%100 != 11 { + return 0 + } + + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return 1 + } + + return 2 + }, + // nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); + "sr_RS": func(n int) int { + if n%10 == 1 && n%100 != 11 { + return 0 + } + + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return 1 + } + + return 2 + }, + // nplurals=1; plural=0; + "zh_CN": func(n int) int { + return 0 + }, +} diff --git a/locale/translations.go b/locale/translations.go new file mode 100644 index 0000000..0aa1aa7 --- /dev/null +++ b/locale/translations.go @@ -0,0 +1,136 @@ +// Code generated by go generate; DO NOT EDIT. +// 2017-11-19 22:01:21.925268372 -0800 PST m=+0.006101515 + +package locale + +var Translations = map[string]string{ + "en_US": `{ + "plural.feed.error_count": [ + "%d error", + "%d errors" + ], + "plural.categories.feed_count": [ + "There is %d feed.", + "There are %d feeds." + ] +}`, + "fr_FR": `{ + "plural.feed.error_count": [ + "%d erreur", + "%d erreurs" + ], + "plural.categories.feed_count": [ + "Il y %d abonnement.", + "Il y %d abonnements." + ], + "Username": "Nom d'utilisateur", + "Password": "Mot de passe", + "Unread": "Non lus", + "History": "Historique", + "Feeds": "Abonnements", + "Categories": "Catégories", + "Settings": "Réglages", + "Logout": "Se déconnecter", + "Next": "Suivant", + "Previous": "Précédent", + "New Subscription": "Nouvel Abonnment", + "Import": "Importation", + "Export": "Exportation", + "There is no category. You must have at least one category.": "Il n'y a aucune catégorie. Vous devez avoir au moins une catégorie.", + "URL": "URL", + "Category": "Catégorie", + "Find a subscription": "Trouver un abonnement", + "Loading...": "Chargement...", + "Create a category": "Créer une catégorie", + "There is no category.": "Il n'y a aucune catégorie.", + "Edit": "Modifier", + "Remove": "Supprimer", + "No feed.": "Aucun abonnement.", + "There is no article in this category.": "Il n'y a aucun article dans cette catégorie.", + "Original": "Original", + "Mark this page as read": "Marquer cette page comme lu", + "not yet": "pas encore", + "just now": "à l'instant", + "1 minute ago": "il y a une minute", + "%d minutes ago": "il y a %d minutes", + "1 hour ago": "il y a une heure", + "%d hours ago": "il y a %d heures", + "yesterday": "hier", + "%d days ago": "il y a %d jours", + "%d weeks ago": "il y a %d semaines", + "%d months ago": "il y a %d mois", + "%d years ago": "il y a %d années", + "Date": "Date", + "IP Address": "Adresse IP", + "User Agent": "Navigateur Web", + "Actions": "Actions", + "Current session": "Session actuelle", + "Sessions": "Sessions", + "Users": "Utilisateurs", + "Add user": "Ajouter un utilisateur", + "Choose a Subscription": "Choisissez un abonnement", + "Subscribe": "S'abonner", + "New Category": "Nouvelle Catégorie", + "Title": "Titre", + "Save": "Sauvegarder", + "or": "ou", + "cancel": "annuler", + "New User": "Nouvel Utilisateur", + "Confirmation": "Confirmation", + "Administrator": "Administrateur", + "Edit Category: %s": "Modification de la catégorie : %s", + "Update": "Mettre à jour", + "Edit Feed: %s": "Modification de l'abonnement : %s", + "There is no category!": "Il n'y a aucune catégorie !", + "Edit user: %s": "Modification de l'utilisateur : %s", + "There is no article for this feed.": "Il n'y a aucun article pour cet abonnement.", + "Add subscription": "Ajouter un abonnement", + "You don't have any subscription.": "Vous n'avez aucun abonnement", + "Last check:": "Dernière vérification :", + "Refresh": "Actualiser", + "There is no history at the moment.": "Il n'y a aucun historique pour le moment.", + "OPML file": "Fichier OPML", + "Sign In": "Connexion", + "Sign in": "Connexion", + "Theme": "Thème", + "Timezone": "Fuseau horaire", + "Language": "Langue", + "There is no unread article.": "Il n'y a rien de nouveau à lire.", + "You are the only user.": "Vous êtes le seul utilisateur.", + "Last Login": "Dernière connexion", + "Yes": "Oui", + "No": "Non", + "This feed already exists (%s).": "Cet abonnement existe déjà (%s).", + "Unable to fetch feed (statusCode=%d).": "Impossible de récupérer cet abonnement (code=%d).", + "Unable to open this link: %v": "Impossible d'ouvrir ce lien : %v", + "Unable to analyze this page: %v": "Impossible d'analyzer cette page : %v", + "Unable to find any subscription.": "Impossible de trouver un abonnement.", + "The URL and the category are mandatory.": "L'URL et la catégorie sont obligatoire.", + "All fields are mandatory.": "Tous les champs sont obligatoire.", + "Passwords are not the same.": "Les mots de passe ne sont pas les mêmes.", + "You must use at least 6 characters.": "Vous devez utiliser au moins 6 caractères.", + "The username is mandatory.": "Le nom d'utilisateur est obligatoire.", + "The username, theme, language and timezone fields are mandatory.": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", + "The title is mandatory.": "Le titre est obligatoire.", + "About": "A propos", + "version": "Version", + "Version:": "Version :", + "Build Date:": "Date de la compilation :", + "Author:": "Auteur :", + "Authors": "Auteurs", + "License:": "Licence :", + "Attachments": "Pièces jointes", + "Download": "Télécharger", + "Invalid username or password.": "Mauvais identifiant ou mot de passe.", + "Never": "Jamais", + "Unable to execute request: %v": "Impossible d'exécuter cette requête: %v", + "Last Parsing Error": "Dernière erreur d'analyse", + "There is a problem with this feed": "Il y a un problème avec cet abonnement" +} +`, +} + +var TranslationsChecksums = map[string]string{ + "en_US": "6fe95384260941e8a5a3c695a655a932e0a8a6a572c1e45cb2b1ae8baa01b897", + "fr_FR": "1f75e5a4b581755f7f84687126bc5b96aaf0109a2f83a72a8770c2ad3ddb7ba3", +} diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json new file mode 100644 index 0000000..0ec7b26 --- /dev/null +++ b/locale/translations/en_US.json @@ -0,0 +1,10 @@ +{ + "plural.feed.error_count": [ + "%d error", + "%d errors" + ], + "plural.categories.feed_count": [ + "There is %d feed.", + "There are %d feeds." + ] +}
\ No newline at end of file diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json new file mode 100644 index 0000000..7699bb7 --- /dev/null +++ b/locale/translations/fr_FR.json @@ -0,0 +1,113 @@ +{ + "plural.feed.error_count": [ + "%d erreur", + "%d erreurs" + ], + "plural.categories.feed_count": [ + "Il y %d abonnement.", + "Il y %d abonnements." + ], + "Username": "Nom d'utilisateur", + "Password": "Mot de passe", + "Unread": "Non lus", + "History": "Historique", + "Feeds": "Abonnements", + "Categories": "Catégories", + "Settings": "Réglages", + "Logout": "Se déconnecter", + "Next": "Suivant", + "Previous": "Précédent", + "New Subscription": "Nouvel Abonnment", + "Import": "Importation", + "Export": "Exportation", + "There is no category. You must have at least one category.": "Il n'y a aucune catégorie. Vous devez avoir au moins une catégorie.", + "URL": "URL", + "Category": "Catégorie", + "Find a subscription": "Trouver un abonnement", + "Loading...": "Chargement...", + "Create a category": "Créer une catégorie", + "There is no category.": "Il n'y a aucune catégorie.", + "Edit": "Modifier", + "Remove": "Supprimer", + "No feed.": "Aucun abonnement.", + "There is no article in this category.": "Il n'y a aucun article dans cette catégorie.", + "Original": "Original", + "Mark this page as read": "Marquer cette page comme lu", + "not yet": "pas encore", + "just now": "à l'instant", + "1 minute ago": "il y a une minute", + "%d minutes ago": "il y a %d minutes", + "1 hour ago": "il y a une heure", + "%d hours ago": "il y a %d heures", + "yesterday": "hier", + "%d days ago": "il y a %d jours", + "%d weeks ago": "il y a %d semaines", + "%d months ago": "il y a %d mois", + "%d years ago": "il y a %d années", + "Date": "Date", + "IP Address": "Adresse IP", + "User Agent": "Navigateur Web", + "Actions": "Actions", + "Current session": "Session actuelle", + "Sessions": "Sessions", + "Users": "Utilisateurs", + "Add user": "Ajouter un utilisateur", + "Choose a Subscription": "Choisissez un abonnement", + "Subscribe": "S'abonner", + "New Category": "Nouvelle Catégorie", + "Title": "Titre", + "Save": "Sauvegarder", + "or": "ou", + "cancel": "annuler", + "New User": "Nouvel Utilisateur", + "Confirmation": "Confirmation", + "Administrator": "Administrateur", + "Edit Category: %s": "Modification de la catégorie : %s", + "Update": "Mettre à jour", + "Edit Feed: %s": "Modification de l'abonnement : %s", + "There is no category!": "Il n'y a aucune catégorie !", + "Edit user: %s": "Modification de l'utilisateur : %s", + "There is no article for this feed.": "Il n'y a aucun article pour cet abonnement.", + "Add subscription": "Ajouter un abonnement", + "You don't have any subscription.": "Vous n'avez aucun abonnement", + "Last check:": "Dernière vérification :", + "Refresh": "Actualiser", + "There is no history at the moment.": "Il n'y a aucun historique pour le moment.", + "OPML file": "Fichier OPML", + "Sign In": "Connexion", + "Sign in": "Connexion", + "Theme": "Thème", + "Timezone": "Fuseau horaire", + "Language": "Langue", + "There is no unread article.": "Il n'y a rien de nouveau à lire.", + "You are the only user.": "Vous êtes le seul utilisateur.", + "Last Login": "Dernière connexion", + "Yes": "Oui", + "No": "Non", + "This feed already exists (%s).": "Cet abonnement existe déjà (%s).", + "Unable to fetch feed (statusCode=%d).": "Impossible de récupérer cet abonnement (code=%d).", + "Unable to open this link: %v": "Impossible d'ouvrir ce lien : %v", + "Unable to analyze this page: %v": "Impossible d'analyzer cette page : %v", + "Unable to find any subscription.": "Impossible de trouver un abonnement.", + "The URL and the category are mandatory.": "L'URL et la catégorie sont obligatoire.", + "All fields are mandatory.": "Tous les champs sont obligatoire.", + "Passwords are not the same.": "Les mots de passe ne sont pas les mêmes.", + "You must use at least 6 characters.": "Vous devez utiliser au moins 6 caractères.", + "The username is mandatory.": "Le nom d'utilisateur est obligatoire.", + "The username, theme, language and timezone fields are mandatory.": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", + "The title is mandatory.": "Le titre est obligatoire.", + "About": "A propos", + "version": "Version", + "Version:": "Version :", + "Build Date:": "Date de la compilation :", + "Author:": "Auteur :", + "Authors": "Auteurs", + "License:": "Licence :", + "Attachments": "Pièces jointes", + "Download": "Télécharger", + "Invalid username or password.": "Mauvais identifiant ou mot de passe.", + "Never": "Jamais", + "Unable to execute request: %v": "Impossible d'exécuter cette requête: %v", + "Last Parsing Error": "Dernière erreur d'analyse", + "There is a problem with this feed": "Il y a un problème avec cet abonnement" +} diff --git a/locale/translator.go b/locale/translator.go new file mode 100644 index 0000000..5560dd6 --- /dev/null +++ b/locale/translator.go @@ -0,0 +1,40 @@ +// Copyright 2017 Frédéric Guillot. All rights reserved. +// Use of this source code is governed by the Apache 2.0 +// license that can be found in the LICENSE file. + +package locale + +import ( + "encoding/json" + "fmt" + "strings" +) + +type Translator struct { + Locales Locales +} + +func (t *Translator) AddLanguage(language, translations string) error { + var decodedTranslations Translation + + decoder := json.NewDecoder(strings.NewReader(translations)) + if err := decoder.Decode(&decodedTranslations); err != nil { + return fmt.Errorf("Invalid JSON file: %v", err) + } + + t.Locales[language] = decodedTranslations + return nil +} + +func (t *Translator) GetLanguage(language string) *Language { + translations, found := t.Locales[language] + if !found { + return &Language{language: language} + } + + return &Language{language: language, translations: translations} +} + +func NewTranslator() *Translator { + return &Translator{Locales: make(Locales)} +} |