Implement localization for API responses and error messages

- Added localization support for API responses in English and Spanish.
- Introduced a middleware to set the API locale based on the Accept-Language header.
- Updated various exception messages and validation responses to use localized strings.
- Created new language files for English and Spanish translations.
- Refactored existing code to replace hardcoded messages with localized strings.
- Added tests to verify localization functionality and response correctness.
This commit is contained in:
2026-07-28 10:19:39 -03:00
parent 754aeebe3a
commit ff9d7728e8
34 changed files with 654 additions and 114 deletions

View File

@@ -41,7 +41,7 @@ class CheckoutService
if ($cartId === null) {
throw ValidationException::withMessages([
'cart_id' => 'A cart or direct item is required.',
'cart_id' => __('api.purchase.source_required'),
]);
}
@@ -64,7 +64,7 @@ class CheckoutService
if ($purchase->payment_method === null) {
throw ValidationException::withMessages([
'payment_method' => 'The purchase payment method must be selected before finalizing.',
'payment_method' => __('api.purchase.payment_method_required'),
]);
}
@@ -102,7 +102,7 @@ class CheckoutService
|| ($purchase->expires_at !== null && $purchase->expires_at->isPast())
) {
throw ValidationException::withMessages([
'purchase' => 'The purchase is no longer editable.',
'purchase' => __('api.purchase.not_editable'),
]);
}
@@ -128,7 +128,7 @@ class CheckoutService
|| ($purchase->expires_at !== null && $purchase->expires_at->isPast())
) {
throw ValidationException::withMessages([
'purchase' => 'The purchase is no longer editable.',
'purchase' => __('api.purchase.not_editable'),
]);
}
@@ -144,7 +144,7 @@ class CheckoutService
if ($purchaseItem->reservation_status !== PurchaseItem::RESERVATION_ACTIVE) {
throw ValidationException::withMessages([
'item' => 'The purchase item is no longer editable.',
'item' => __('api.purchase.item_not_editable'),
]);
}
@@ -162,7 +162,7 @@ class CheckoutService
}
} catch (\InvalidArgumentException $exception) {
throw ValidationException::withMessages([
'quantity' => 'No hay suficiente stock disponible.',
'quantity' => __('api.purchase.insufficient_stock'),
]);
}
@@ -197,7 +197,7 @@ class CheckoutService
|| ($purchase->expires_at !== null && $purchase->expires_at->isPast())
) {
throw ValidationException::withMessages([
'purchase' => 'The purchase is no longer editable.',
'purchase' => __('api.purchase.not_editable'),
]);
}
@@ -233,7 +233,7 @@ class CheckoutService
Purchase::STATUS_EXPIRED,
], true)) {
throw ValidationException::withMessages([
'purchase' => 'A cancelled, rejected or expired purchase cannot be confirmed.',
'purchase' => __('api.purchase.cannot_confirm'),
]);
}
@@ -249,7 +249,7 @@ class CheckoutService
$this->catalogInventoryService->commit($selection, (int) $item->cantidad);
} catch (\InvalidArgumentException $exception) {
throw ValidationException::withMessages([
'items' => 'The purchase has an inconsistent stock reservation.',
'items' => __('api.purchase.inconsistent_reservation'),
]);
}
@@ -306,7 +306,7 @@ class CheckoutService
}
throw ValidationException::withMessages([
'purchase' => 'A paid purchase cannot be cancelled.',
'purchase' => __('api.purchase.paid_cannot_cancel'),
]);
}
@@ -339,7 +339,7 @@ class CheckoutService
$this->catalogInventoryService->release($selection, (int) $item->cantidad);
} catch (\InvalidArgumentException $exception) {
throw ValidationException::withMessages([
'items' => 'The purchase has an inconsistent stock reservation.',
'items' => __('api.purchase.inconsistent_reservation'),
]);
}
}
@@ -375,7 +375,7 @@ class CheckoutService
if ($availableQuantity !== null && $availableQuantity < $quantity) {
throw ValidationException::withMessages([
'direct_item.cantidad' => "Stock insuficiente. Maximo disponible: {$availableQuantity}.",
'direct_item.cantidad' => __('api.purchase.direct_item_max_stock', ['max' => $availableQuantity]),
]);
}
@@ -383,7 +383,7 @@ class CheckoutService
$this->catalogInventoryService->reserve($selection, $quantity);
} catch (\InvalidArgumentException $exception) {
throw ValidationException::withMessages([
'direct_item.cantidad' => 'No hay suficiente stock disponible.',
'direct_item.cantidad' => __('api.purchase.insufficient_stock'),
]);
}
@@ -416,7 +416,7 @@ class CheckoutService
if ($cartItems->isEmpty()) {
throw ValidationException::withMessages([
'cart_id' => 'The selected cart does not contain items.',
'cart_id' => __('api.purchase.empty_cart'),
]);
}
@@ -596,13 +596,13 @@ class CheckoutService
foreach ($cartItems as $item) {
if ($item->selectedItem() === null) {
throw ValidationException::withMessages([
'cart_id' => 'One or more catalog items could not be loaded.',
'cart_id' => __('api.purchase.catalog_item_missing'),
]);
}
if ($item->catalogItem?->tenant_code !== $tenant->codigo) {
throw ValidationException::withMessages([
'cart_id' => 'One or more catalog items do not belong to the tenant.',
'cart_id' => __('api.purchase.catalog_item_wrong_tenant'),
]);
}
}
@@ -621,7 +621,7 @@ class CheckoutService
if ($cart->status !== 'active') {
throw ValidationException::withMessages([
'cart_id' => 'The selected cart is no longer active.',
'cart_id' => __('api.purchase.inactive_cart'),
]);
}
@@ -682,13 +682,13 @@ class CheckoutService
if ($catalogItem->isBundle()) {
if ($variantId !== null) {
throw ValidationException::withMessages([
'direct_item.variant_id' => 'Un bundle no admite una variante.',
'direct_item.variant_id' => __('api.cart.bundle_variant_forbidden'),
]);
}
if (! $catalogItem->bundleComponents()->exists()) {
throw ValidationException::withMessages([
'direct_item.catalog_item_id' => 'El bundle no tiene componentes.',
'direct_item.catalog_item_id' => __('api.cart.empty_bundle'),
]);
}
@@ -698,7 +698,7 @@ class CheckoutService
if ($variantId === null) {
if ($catalogItem->inventory_id === null) {
throw ValidationException::withMessages([
'direct_item.variant_id' => 'Debe seleccionar una variante para este item.',
'direct_item.variant_id' => __('api.cart.variant_required'),
]);
}