Fix FLEXIAPI-179 Add Localization support as a Middleware that handles Accept-Language HTTP header

This commit is contained in:
Timothée Jaussoin 2024-06-03 13:52:17 +00:00
parent afe29811ac
commit 0f3454fb68
11 changed files with 474 additions and 1 deletions

View file

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

View file

@ -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,
];
/**

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
class Localization
{
public function handle(Request $request, Closure $next)
{
$localization = $request->header('Accept-Language');
$localization = in_array($localization, config('app.authorized_locales'), true)
? $localization
: config('app.locale');
App::setLocale($localization);
return $next($request);
}
}

View file

@ -149,6 +149,7 @@ return [
*/
'locale' => 'en',
'authorized_locales' => ['fr', 'en'],
/*
|--------------------------------------------------------------------------

View file

@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => '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.',
];

View file

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Précédent',
'next' => 'Suivant &raquo;',
];

View file

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
"reset" => "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.",
];

View file

@ -0,0 +1,307 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| such as the size rules. Feel free to tweak each of these messages.n
|
*/
'accepted' => '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",
],
];

View file

@ -25,6 +25,31 @@ The endpoints are accessible using three different models:
- <span class="badge badge-info">User</span> the endpoint can only be accessed by an authenticated user
- <span class="badge badge-warning">Admin</span> 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 <a href="#get-accountsmeapikey">the dedicated API endpoint</a>.

View file

@ -20,7 +20,6 @@
namespace Tests\Feature;
use App\Account;
use App\ApiKey;
use App\Password;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;

View file

@ -0,0 +1,55 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
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/>.
*/
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.']]);
}
}