1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-07-27 17:28:38 +00:00

First commit

This commit is contained in:
Frédéric Guillot 2017-11-19 21:10:04 -08:00
commit 8ffb773f43
2121 changed files with 1118910 additions and 0 deletions

47
locale/language.go Normal file
View file

@ -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...)
}

30
locale/locale.go Normal file
View file

@ -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",
}
}

103
locale/locale_test.go Normal file
View file

@ -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)
}
}

101
locale/plurals.go Normal file
View file

@ -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
},
}

136
locale/translations.go Normal file
View file

@ -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",
}

View file

@ -0,0 +1,10 @@
{
"plural.feed.error_count": [
"%d error",
"%d errors"
],
"plural.categories.feed_count": [
"There is %d feed.",
"There are %d feeds."
]
}

View file

@ -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"
}

40
locale/translator.go Normal file
View file

@ -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)}
}