diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5281829..2de8144 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
v1.5
----
- Fix FLEXIAPI-180 Fix the token and activation flow for the provisioning with token endpoint when the header is missing
+- Fix FLEXIAPI-179 Add Localization support as a Middleware that handles Accept-Language HTTP header
- Fix FLEXIAPI-178 Show the unused code in the Activity tab of the accounts in the admin panel
- Fix FLEXIAPI-177 Complete vcards-storage and devices related endpoints with their User/Admin ones
- Fix FLEXIAPI-176 Improve logs for the deprecated endpoints and AccountCreationToken related serialization
diff --git a/flexiapi/app/Http/Kernel.php b/flexiapi/app/Http/Kernel.php
index 7470fe9..de54266 100644
--- a/flexiapi/app/Http/Kernel.php
+++ b/flexiapi/app/Http/Kernel.php
@@ -57,6 +57,7 @@ class Kernel extends HttpKernel
'api' => [
'throttle:600,1', // move to 600 instead of 60
'bindings',
+ 'localization',
],
];
@@ -85,6 +86,7 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
+ 'localization' => \App\Http\Middleware\Localization::class,
];
/**
diff --git a/flexiapi/app/Http/Middleware/Localization.php b/flexiapi/app/Http/Middleware/Localization.php
new file mode 100644
index 0000000..2b63eb3
--- /dev/null
+++ b/flexiapi/app/Http/Middleware/Localization.php
@@ -0,0 +1,22 @@
+header('Accept-Language');
+ $localization = in_array($localization, config('app.authorized_locales'), true)
+ ? $localization
+ : config('app.locale');
+
+ App::setLocale($localization);
+
+ return $next($request);
+ }
+}
diff --git a/flexiapi/config/app.php b/flexiapi/config/app.php
index 55d7831..c16f767 100644
--- a/flexiapi/config/app.php
+++ b/flexiapi/config/app.php
@@ -149,6 +149,7 @@ return [
*/
'locale' => 'en',
+ 'authorized_locales' => ['fr', 'en'],
/*
|--------------------------------------------------------------------------
diff --git a/flexiapi/resources/lang/fr/auth.php b/flexiapi/resources/lang/fr/auth.php
new file mode 100644
index 0000000..22ea45a
--- /dev/null
+++ b/flexiapi/resources/lang/fr/auth.php
@@ -0,0 +1,20 @@
+ 'Ces informations d\'identification ne correspondent pas à nos enregistrements.',
+ 'password' => 'Le mot de passe fourni est incorrect.',
+ 'throttle' => 'Trop de tentatives de connexion. Veuillez essayer de nouveau dans :seconds secondes.',
+
+];
diff --git a/flexiapi/resources/lang/fr/pagination.php b/flexiapi/resources/lang/fr/pagination.php
new file mode 100644
index 0000000..8eff374
--- /dev/null
+++ b/flexiapi/resources/lang/fr/pagination.php
@@ -0,0 +1,19 @@
+ '« Précédent',
+ 'next' => 'Suivant »',
+
+];
diff --git a/flexiapi/resources/lang/fr/passwords.php b/flexiapi/resources/lang/fr/passwords.php
new file mode 100644
index 0000000..8dea534
--- /dev/null
+++ b/flexiapi/resources/lang/fr/passwords.php
@@ -0,0 +1,22 @@
+ "Votre mot de passe a été réinitialisé !",
+ "sent" => "Nous vous avons envoyé par mail le lien de réinitialisation du mot de passe !",
+ 'throttled' => 'Veuillez patienter avant de réessayer.',
+ "token" => "Ce jeton de réinitialisation du mot de passe n'est pas valide.",
+ "user" => "Aucun utilisateur n'a été trouvé avec cette adresse e-mail.",
+
+];
diff --git a/flexiapi/resources/lang/fr/validation.php b/flexiapi/resources/lang/fr/validation.php
new file mode 100644
index 0000000..ae36f0a
--- /dev/null
+++ b/flexiapi/resources/lang/fr/validation.php
@@ -0,0 +1,307 @@
+ 'Le champ :attribute doit être accepté.',
+ 'accepted_if' => 'Le champ :attribute doit être accepté lorsque :other vaut :value.',
+ 'active_url' => 'Le champ :attribute n\'est pas une URL valide.',
+ 'after' => 'Le champ :attribute doit être une date postérieure au :date.',
+ 'after_or_equal' => 'Le champ :attribute doit être une date après ou égale à :date.',
+ 'alpha' => 'Le champ :attribute doit seulement contenir des lettres.',
+ 'alpha_dash' => 'Le champ :attribute doit seulement contenir des lettres, des chiffres et des tirets.',
+ 'alpha_num' => 'Le champ :attribute doit seulement contenir des chiffres et des lettres.',
+ 'array' => 'Le champ :attribute doit être un tableau.',
+ 'before' => 'Le champ :attribute doit être une date antérieure au :date.',
+ 'before_or_equal' => 'Le champ :attribute: doit être une date avant ou égale à :date.',
+ 'between' => [
+ 'numeric' => 'La valeur de :attribute doit être comprise entre :min et :max.',
+ 'file' => 'Le fichier :attribute doit avoir une taille entre :min et :max kilo-octets.',
+ 'string' => 'Le texte :attribute doit avoir entre :min et :max caractères.',
+ 'array' => 'Le tableau :attribute doit avoir entre :min et :max éléments.',
+ ],
+ 'boolean' => 'Le champ :attribute doit être vrai ou faux.',
+ 'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.',
+ 'current_password' => 'Le mot de passe est incorrect.',
+ 'date' => 'Le champ :attribute n\'est pas une date valide.',
+ 'date_equals' => 'Le champ :attribute doit être une date égale à :date.',
+ 'date_format' => 'Le champ :attribute ne correspond pas au format :format.',
+ 'declined' => 'Le champ :attribute doit être refusé.',
+ 'declined_if' => 'Le champ :attribute doit être refusé lorsque :other vaut :value.',
+ 'different' => 'Les champs :attribute et :other doivent être différents.',
+ 'digits' => 'Le champ :attribute doit avoir :digits chiffres.',
+ 'digits_between' => 'Le champ :attribute doit avoir entre :min and :max chiffres.',
+ 'dimensions' => 'Le champ :attribute a des dimensions d\'image non valides.',
+ 'distinct' => 'Le champ a une valeur en double.',
+ 'doesnt_end_with' => 'Le champ :attribute ne peut pas se terminer par l\'un des éléments suivants : :values.',
+ 'doesnt_start_with' => 'Le champ :attribute ne peut pas se commencer par l\'un des éléments suivants : :values.',
+ 'email' => "Le champ :attribute doit être une adresse email valide.",
+ 'ends_with' => 'Le champ :attribute doit se terminer par l\'une des valeurs suivantes : :values.',
+ 'enum' => 'Le champ :attribute sélectionné est invalide.',
+ 'exists' => 'Le champ :attribute sélectionné est invalide.',
+ 'file' => 'Le champ :attribute doit être un fichier.',
+ 'filled' => "Le champ :attribute est obligatoire.",
+ 'gt' => [
+ 'numeric' => 'Le champ :attribute doit être supérieur à :value.',
+ 'file' => 'Le champ :attribute doit être supérieur à :value kilobytes.',
+ 'string' => 'Le champ :attribute doit être supérieur à :value caractères.',
+ 'array' => 'Le champ :attribute doit avoir plus de :value éléments.',
+ ],
+ 'gte' => [
+ 'numeric' => 'Le champ :attribute doit être supérieur ou égal à :value.',
+ 'file' => 'Le champ :attribute doit être supérieur ou égal à :value kilobytes.',
+ 'string' => 'Le champ :attribute doit être supérieur ou égal à :value caractères.',
+ 'array' => 'Le champ :attribute doit avoir :value éléments ou plus.',
+ ],
+ 'image' => 'Le champ :attribute doit être une image.',
+ 'in' => 'Le champ :attribute est invalide.',
+ 'in_array' => 'Le champ :attribute n\'existe pas dans :other.',
+ 'integer' => 'Le champ :attribute doit être un entier (un nombre sans virgule).',
+ 'ip' => 'Le champ :attribute doit être une adresse IP valide.',
+ 'ipv4' => 'Le champ :attribute doit être une adresse IPv4 valide.',
+ 'ipv6' => 'Le champ :attribute doit être une adresse IPv6 valide.',
+ 'json' => 'Le champ :attribute doit être une chaîne JSON valide.',
+ 'lowercase' => 'Le champ :attribute doit être en minuscules.',
+ 'lt' => [
+ 'numeric' => 'Le champ :attribute doit être inférieur à :value.',
+ 'file' => 'Le champ :attribute doit être inférieur à :value kilobytes.',
+ 'string' => 'Le champ :attribute doit être inférieur à :value caractères.',
+ 'array' => 'Le champ :attribute doit avoir moins de :value éléments.',
+ ],
+ 'lte' => [
+ 'numeric' => 'Le champ :attribute doit être inférieur ou égal à :value.',
+ 'file' => 'Le champ :attribute doit être inférieur ou égal à :value kilobytes.',
+ 'string' => 'Le champ :attribute doit être inférieur ou égal à :value caractères.',
+ 'array' => 'Le champ :attribute ne doit pas avoir plus de :value éléments.',
+ ],
+ 'mac_address' => 'Le champ :attribute doit être une adresse MAC valide.',
+ "max" => [
+ 'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.',
+ 'file' => 'La taille du fichier :attribute ne peut être supérieure à :max kilo-octets.',
+ 'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.',
+ 'array' => 'Le tableau :attribute ne peut avoir plus de :max éléments.',
+ ],
+ 'max_digits' => 'Le champ :attribute ne doit pas avoir plus de :max chiffres.',
+ 'mimes' => 'Le champ :attribute doit être un fichier de type : :values.',
+ 'mimetypes' => 'Le champ :attribute doit être un fichier de type : :values.',
+ 'min' => [
+ 'numeric' => 'La valeur de :attribute ne peut être inférieure à :min.',
+ 'file' => 'La taille du fichier :attribute ne peut être inférieure à :min kilo-octets.',
+ 'string' => 'Le texte du champ :attribute doit contenir au moins :min caractères.',
+ 'array' => 'Le tableau :attribute doit avoir au moins :min éléments.',
+ ],
+ 'min_digits' => 'Le champ :attribute doit avoir au moins :min chiffres.',
+ 'multiple_of' => 'Le champ :attribute doit être un multiple de :value.',
+ 'not_in' => 'Le champ :attribute sélectionné n\'est pas valide.',
+ 'not_regex' => 'Le champ :attribute a un format invalide.',
+ 'numeric' => 'Le champ :attribute doit contenir un nombre.',
+ 'password' => [
+ 'letters' => 'Le champ :attribute doit contenir au moins une lettre.',
+ 'mixed' => 'Le champ :attribute doit contenir au moins une majuscule et une minuscule.',
+ 'numbers' => 'Le champ :attribute doit contenir au moins un chiffre.',
+ 'symbols' => 'Le champ :attribute doit contenir au moins un symbole.',
+ 'uncompromised' => 'Le :attribute donné est apparu dans une fuite de données. Veuillez choisir un autre :attribute.',
+ ],
+ 'present' => 'Le champ :attribute doit être present.',
+ 'prohibited' => 'Le champ :attribute est interdit.',
+ 'prohibited_if' => 'Le champ :attribute est interdit lorsque :other vaut :value.',
+ 'prohibited_unless' => 'Le champ :attribute est interdit sauf si :other est dans :values.',
+ 'prohibits' => 'Le champ :attribute interdit la présence de :other.',
+ 'regex' => 'Le format du champ :attribute est invalide.',
+ 'required' => 'Le champ :attribute est obligatoire.',
+ 'required_array_keys' => 'Le champ :attribute doit contenir des entrées pour : :values.',
+ 'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.',
+ 'required_if_accepted' => 'Le champ :attribute est obligatoire lorsque :other est accepté.',
+ 'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est dans :values.',
+ 'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.',
+ 'required_with_all' => 'Le champ :attribute est obligatoire quand :values est présent.',
+ 'required_without' => 'Le champ :attribute est obligatoire quand :values n\'est pas présent.',
+ 'required_without_all' => 'Le champ :attribute est requis quand aucun de :values n\'est présent.',
+ 'same' => 'Les champs :attribute et :other doivent être identiques.',
+ 'size' => [
+ 'numeric' => 'La valeur de :attribute doit être :size.',
+ 'file' => 'La taille du fichier de :attribute doit être de :size kilo-octets.',
+ 'string' => 'Le texte de :attribute doit contenir :size caractères.',
+ 'array' => 'Le tableau :attribute doit contenir :size éléments.',
+ ],
+ 'starts_with' => 'Le :attribute doit commencer par l\'un des éléments suivants : :values.',
+ 'string' => 'Le :attribute doit être une chaîne.',
+ 'timezone' => 'Le :attribute doit être un fuseau horaire valide.',
+ 'unique' => 'Le :attribute a déjà été pris.',
+ 'uploaded' => 'Le :attribute n\'a pas pu être téléchargé.',
+ 'uppercase' => 'Le :attribute doit être en majuscule.',
+ 'url' => 'Le :attribute doit être une URL valide.',
+ 'uuid' => 'Le :attribute doit être un UUID valide.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify custom validation messages for attributes using the
+ | convention "attribute.rule" to name the lines. This makes it quick to
+ | specify a specific custom language line for a given attribute rule.
+ |
+ */
+
+ 'custom' => [
+ 'attribute-name' => [
+ 'rule-name' => 'custom-message',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Attributes
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used to swap our attribute placeholder
+ | with something more reader friendly such as "E-Mail Address" instead
+ | of "email". This simply helps us make our message more expressive.
+ |
+ */
+
+ 'attributes' => [
+ "activity" => "activité",
+ "activities" => "activités",
+ "address" => "adresse",
+ "addresses" => "adresses",
+ "age" => "âge",
+ "ages" => "âges",
+ "amount" => "montant",
+ "amounts" => "montants",
+ "answer" => "réponse",
+ "answers" => "réponses",
+ "available" => "disponible",
+ "availables" => "disponibles",
+ "barcode" => "code-barres",
+ "barcodes" => "codes-barres",
+ "birth_date" => "date de naissance",
+ "brand" => "marque",
+ "brands" => "marques",
+ "brand_name" => "nom de la marque",
+ "buying_price" => "prix d'achat",
+ "category" => "catégorie",
+ "categories" => "catégories",
+ "city" => "ville",
+ "cities" => "villes",
+ "civility" => "civilité",
+ "civilities" => "civilités",
+ "comment" => "commentaire",
+ "comments" => "commentaires",
+ "company" => "entreprise",
+ "companies" => "entreprises",
+ "confirmed" => "confirmé",
+ "confirmed_at" => "confirmé le",
+ "content" => "contenu",
+ "contents" => "contenus",
+ "country" => "pays",
+ "countries" => "pays",
+ "customer" => "client",
+ "customers" => "clients",
+ "day" => "jour",
+ "days" => "jours",
+ "date_end" => "date de fin",
+ "date_start" => "date de début",
+ "directory" => "dossier",
+ "directory_name" => "nom du dossier",
+ "directories" => "dossiers",
+ "directories_name" => "nom des dossiers",
+ "directories_names" => "noms des dossiers",
+ "email_banned" => "email banni",
+ "email_confirmed" => "email confirmé",
+ "email_validated" => "email validé",
+ "email_prohibited" => "email inerdit",
+ "emails_banned" => "emails bannis",
+ "emails_confirmed" => "emails confirmés",
+ "emails_validated" => "emails validés",
+ "emails_prohibited" => "emails inerdits",
+ "file" => "fichier",
+ "files" => "fichiers",
+ "first_name" => "prénom",
+ "first_names" => "prénoms",
+ "gender" => "genre",
+ "genders" => "genres",
+ "hour" => "heure",
+ "hours" => "heures",
+ "is_active" => "est actif ?",
+ "is_banned" => "bannir ?",
+ "job" => "métier",
+ "jobs" => "métiers",
+ "last_name" => "nom de famille",
+ "last_names" => "noms de famille",
+ "link" => "lien",
+ "links" => "liens",
+ "month" => "mois",
+ "name" => "nom",
+ "names" => "noms",
+ "office" => "bureau",
+ "offices" => "bureaux",
+ "other" => "autre",
+ "others" => "autres",
+ "paid_at" => "payé le",
+ "password" => "mot de passe",
+ "password_confirmation" => "confirmation du mot de passe",
+ "password_current" => "mot de passe actuel",
+ "passwords" => "mots de passe",
+ "phone" => "téléphone",
+ "phones" => "téléphones",
+ "postal_code" => "code postal",
+ "price" => "prix",
+ "published_at" => "publié le",
+ "quantity" => "quantité",
+ "quantities" => "quantités",
+ "rate" => "taux",
+ "rates" => "taux",
+ "response" => "réponse",
+ "responses" => "réponses",
+ "role" => "rôle",
+ "roles" => "rôles",
+ "second" => "seconde",
+ "seconds" => "secondes",
+ "siren_number" => "numéro de siren",
+ "siret_number" => "numéro de siret",
+ "size" => "taille",
+ "sizes" => "tailles",
+ "status" => "statut",
+ "statuses" => "statuts",
+ "street" => "rue",
+ "subfolder" => "sous-dossier",
+ "subfolders" => "sous-dossiers",
+ "subdirectory" => "sous-dossier",
+ "subdirectories" => "sous-dossiers",
+ "subject" => "sujet",
+ "subjects" => "sujets",
+ "summary" => "chapô",
+ "summarys" => "chapôs",
+ "supplier" => "fournisseur",
+ "suppliers" => "fournisseurs",
+ "tax" => "taxe",
+ "time" => "heure",
+ "title" => "titre",
+ "titles" => "titres",
+ "user" => "utilisateur",
+ "users" => "utilisateurs",
+ "username" => "pseudo",
+ "usernames" => "pseudos",
+ "value" => "valeur",
+ "values" => "valeurs",
+ "vat" => "TVA",
+ "vat_rate" => "taux de TVA",
+ "website" => "site web",
+ "websites" => "sites web",
+ "year" => "année",
+ "years" => "années",
+ ],
+
+];
diff --git a/flexiapi/resources/views/api/documentation_markdown.blade.php b/flexiapi/resources/views/api/documentation_markdown.blade.php
index e4444f8..d763cf6 100644
--- a/flexiapi/resources/views/api/documentation_markdown.blade.php
+++ b/flexiapi/resources/views/api/documentation_markdown.blade.php
@@ -25,6 +25,31 @@ The endpoints are accessible using three different models:
- User the endpoint can only be accessed by an authenticated user
- Admin the endpoint can be only be accessed by an authenticated admin user
+### Localization
+
+You can add an [`Accept-Language`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language) header to your request to translate the responses, and especially errors messages, in a specific language.
+
+Currently supported languages: @php
+ echo implode(', ', config('app.authorized_locales'))
+@endphp
+
+```
+> GET /api/{endpoint}
+> Accept-Language: fr
+> …
+
+< HTTP 422
+< {
+< "message": "Le champ pseudo est obligatoire.",
+< "errors": {
+< "username": [
+< 0 => "Le champ pseudo est obligatoire."
+< ]
+< }
+< }
+
+```
+
### Using the API Key
You can retrieve an API Key from @if (config('app.web_panel')) [your account panel]({{ route('account.login') }}) @else your account panel @endif or using the dedicated API endpoint.
diff --git a/flexiapi/tests/Feature/ApiAccountApiKeyTest.php b/flexiapi/tests/Feature/ApiAccountApiKeyTest.php
index 057daf3..d53cbaf 100644
--- a/flexiapi/tests/Feature/ApiAccountApiKeyTest.php
+++ b/flexiapi/tests/Feature/ApiAccountApiKeyTest.php
@@ -20,7 +20,6 @@
namespace Tests\Feature;
use App\Account;
-use App\ApiKey;
use App\Password;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
diff --git a/flexiapi/tests/Feature/ApiLocalizationTest.php b/flexiapi/tests/Feature/ApiLocalizationTest.php
new file mode 100644
index 0000000..b169b6e
--- /dev/null
+++ b/flexiapi/tests/Feature/ApiLocalizationTest.php
@@ -0,0 +1,55 @@
+.
+*/
+
+namespace Tests\Feature;
+
+use App\Password;
+use Tests\TestCase;
+
+class ApiLocalizationTest extends TestCase
+{
+ protected $route = '/api/accounts';
+ protected $method = 'POST';
+
+ public function testUsernameNotPhone()
+ {
+ $password = Password::factory()->admin()->create();
+ $password->account->generateApiKey();
+
+ $this->keyAuthenticated($password->account)
+ ->withHeaders([
+ 'Accept-Language' => 'de',
+ ])
+ ->json($this->method, $this->route, [
+ 'domain' => 'example.com',
+ ])
+ ->assertJsonValidationErrors(['username'])
+ ->assertJsonFragment(['username' => ['The username field is required.']]);
+
+ $this->keyAuthenticated($password->account)
+ ->withHeaders([
+ 'Accept-Language' => 'fr',
+ ])
+ ->json($this->method, $this->route, [
+ 'domain' => 'example.com',
+ ])
+ ->assertJsonValidationErrors(['username'])
+ ->assertJsonFragment(['username' => ['Le champ pseudo est obligatoire.']]);
+ }
+}