aboutsummaryrefslogtreecommitdiffhomepage
path: root/locale
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2017-11-19 21:10:04 -0800
committerGravatar Frédéric Guillot <fred@miniflux.net>2017-11-19 22:01:46 -0800
commit8ffb773f43c8dc54801ca1d111854e7e881c93c9 (patch)
tree38133a2fc612597a75fed1d13e5b4042f58a2b7e /locale
First commit
Diffstat (limited to 'locale')
-rw-r--r--locale/language.go47
-rw-r--r--locale/locale.go30
-rw-r--r--locale/locale_test.go103
-rw-r--r--locale/plurals.go101
-rw-r--r--locale/translations.go136
-rw-r--r--locale/translations/en_US.json10
-rw-r--r--locale/translations/fr_FR.json113
-rw-r--r--locale/translator.go40
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)}
+}