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.']]); + } +}