@cartbase/storefront 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,467 @@
1
+ import type { StorefrontLocale } from "./types"
2
+
3
+ /**
4
+ * SPANISH — neutral European Spanish (es-ES), readable across markets.
5
+ *
6
+ * A locale pack, opted into by subpath import, never library copy:
7
+ *
8
+ * import { es } from "@cartbase/storefront/locales/es"
9
+ *
10
+ * Typed against the six area shapes, so a label added upstream breaks this
11
+ * file at compile time rather than falling back to English in the middle
12
+ * of a Spanish checkout.
13
+ *
14
+ * THE TERMINOLOGY IS FIXED, not per translator: carrito, pago, pedido,
15
+ * envío, entrega, tarjeta regalo, valoración, Agotado, Añadir al carrito,
16
+ * Seguir comprando. Never usted; the shopper is addressed with the
17
+ * infinitive or the tú imperative. Buttons carry no final period. Where a
18
+ * label is a button or a chip the shortest correct wording wins, because
19
+ * Spanish runs longer than English and the component was laid out in
20
+ * English.
21
+ *
22
+ * Two readings worth knowing, both deliberate: the sort options spell the
23
+ * relation in words ("Precio: de menor a mayor") because an arrow between
24
+ * two values is banned in our UI; and "IVA" is used where the English says
25
+ * VAT, with "Impuestos" kept for the generic word.
26
+ *
27
+ * 249 keys, first written 2026-09-13.
28
+ */
29
+ export const es: StorefrontLocale = {
30
+ code: "es",
31
+ name: "Español",
32
+ products: {
33
+ selectOption: "Elige",
34
+ selectVariant: "Elige variante",
35
+ outOfStock: "Agotado",
36
+ addToCart: "Añadir al carrito",
37
+ from: "Desde",
38
+ original: "Antes:",
39
+ productInformation: "Información del producto",
40
+ shippingAndReturns: "Envío y devoluciones",
41
+ material: "Material",
42
+ countryOfOrigin: "País de origen",
43
+ type: "Tipo",
44
+ weight: "Peso",
45
+ dimensions: "Dimensiones",
46
+ fastDelivery: "Entrega rápida",
47
+ fastDeliveryDescription:
48
+ "Tu paquete llegará en 3-5 días laborables a tu punto de recogida o a la comodidad de tu casa.",
49
+ simpleExchanges: "Cambios sencillos",
50
+ simpleExchangesDescription:
51
+ "¿No te queda del todo bien? Sin problema, cambiamos tu producto por uno nuevo.",
52
+ easyReturns: "Devoluciones fáciles",
53
+ easyReturnsDescription:
54
+ "Solo tienes que devolver tu producto y te reembolsamos el dinero. Sin preguntas: haremos todo lo posible para que tu devolución sea sencilla.",
55
+ relatedProducts: "Productos relacionados",
56
+ relatedProductsDescription:
57
+ "Quizá también te interesen estos productos.",
58
+ },
59
+ store: {
60
+ allProducts: "Todos los productos",
61
+ sortBy: "Ordenar por",
62
+ latestArrivals: "Novedades",
63
+ priceLowToHigh: "Precio: de menor a mayor",
64
+ priceHighToLow: "Precio: de mayor a menor",
65
+ searchTitle: "Búsqueda",
66
+ searchPlaceholder: "Buscar productos…",
67
+ searchAction: "Buscar",
68
+ resultsFor: "Resultados para",
69
+ resultsCount: "resultados",
70
+ noResults: "No se han encontrado productos",
71
+ filters: "Filtros",
72
+ clearFilters: "Borrar filtros",
73
+ },
74
+ cartDrawer: {
75
+ yourCart: "Tu carrito",
76
+ item: "artículo",
77
+ items: "artículos",
78
+ closeCart: "Cerrar carrito",
79
+
80
+ emptyCartTitle: "Tu carrito está vacío",
81
+ emptyCartMessage: "Todavía no has añadido ningún producto.",
82
+ browseProducts: "Ver productos",
83
+
84
+ awayFromFreeShipping: "para el envío gratis",
85
+ allRewardsUnlocked: "Todas las recompensas desbloqueadas",
86
+
87
+ addGiftWrap: "Añadir envoltorio de regalo",
88
+ giftNote: "Nota de regalo",
89
+ orderNote: "Nota del pedido",
90
+ giftNotePlaceholder: "Añade un mensaje personal...",
91
+ orderNotePlaceholder: "Instrucciones especiales para este pedido...",
92
+
93
+ estimatedRewardPoints: "Puntos de recompensa estimados",
94
+
95
+ pairsWellWith: "Combina bien con",
96
+ youllLoveThis: "Te va a encantar",
97
+ youMightAlsoLike: "También te puede gustar",
98
+ addToCart: "Añadir",
99
+
100
+ subtotal: "Subtotal",
101
+ taxAndShipping: "IVA incluido. El envío se calcula en el pago.",
102
+ shipping: "Envío",
103
+ free: "Gratis",
104
+ calculatedAtCheckout: "Se calcula en el pago",
105
+ total: "Total",
106
+ secureCheckout: "Finalizar compra",
107
+ shippingAndTaxCalculatedAtCheckout: "El envío y los impuestos se calculan en el pago",
108
+
109
+ discount: "Descuento",
110
+ tax: "IVA",
111
+ paymentMethodFee: "Comisión por método de pago",
112
+ giftCard: "Tarjeta regalo",
113
+ remainingToPay: "Pendiente de pago",
114
+
115
+ continueShopping: "Seguir comprando",
116
+
117
+ remove: "Eliminar",
118
+ },
119
+ checkout: {
120
+ deliveryInfo: "Información de entrega",
121
+ shippingServices: "Método de envío",
122
+ paymentMethod: "Método de pago",
123
+ orderSummary: "Resumen del pedido",
124
+
125
+ email: "Correo electrónico",
126
+ country: "País",
127
+ singleCountryName: undefined,
128
+ firstName: "Nombre",
129
+ lastName: "Apellidos",
130
+ address: "Dirección",
131
+ apartment: "Apartamento, piso, etc. (opcional)",
132
+ city: "Ciudad",
133
+ postalCode: "Código postal",
134
+ phone: "Teléfono",
135
+ province: "Estado / provincia",
136
+
137
+ saveAddress: "Guardar y continuar",
138
+ updateAddress: "Actualizar dirección",
139
+ addToOrder: "+ Añadir al pedido",
140
+ addCode: "Aplicar",
141
+
142
+ deliveryDisabled:
143
+ "Introduce tu dirección de entrega para ver las opciones de envío disponibles.",
144
+ noShippingOptions: "No hay opciones de envío disponibles para tu dirección.",
145
+ paymentDisabled: "Elige un método de envío para continuar.",
146
+ deliveryPreviewHint: "Introduce tu dirección para elegir un método de entrega.",
147
+ buyNeedsAddress: "Introduce tu dirección de entrega",
148
+ buyNeedsDelivery: "Elige un método de entrega",
149
+
150
+ codNote: "Contra reembolso. Pueden aplicarse cargos adicionales.",
151
+ payOnline: "Pago online",
152
+ cashOnDelivery: "Contra reembolso",
153
+ termsText:
154
+ "Al realizar tu pedido aceptas nuestros Términos y la Política de privacidad.",
155
+
156
+ useSavedAddress: "¿Usar una dirección guardada?",
157
+ saveInfo: "Guardar estos datos para la próxima vez",
158
+
159
+ chooseAddress: "Elige una dirección",
160
+
161
+ items: "artículos",
162
+ qty: "Cant.",
163
+ subtotal: "Subtotal",
164
+ shipping: "Envío",
165
+ shippingCalc: "Se calcula en el pago",
166
+ shippingFree: "Gratis",
167
+ tax: "IVA incl.",
168
+ taxTooltip: "El IVA del 20% está incluido en los precios",
169
+ total: "Total",
170
+ discount: "Descuento",
171
+ discountCode: "Código de descuento",
172
+ discountSub: "Ahorra con un código promocional",
173
+ paymentMethodFee: "Comisión del método de pago",
174
+ recommended: "Recomendado para ti",
175
+ recommendedSoon: "Próximamente",
176
+ recommendedSoonSub: "Recomendaciones personalizadas",
177
+ remove: "Quitar",
178
+ secureCheckout: "Pago seguro",
179
+
180
+ giftCard: "Tarjeta regalo",
181
+ giftCardSub: "Canjea una tarjeta regalo",
182
+ giftCardPlaceholder: "GIFT-XXXX-XXXX",
183
+ giftCardApplied: "Tarjetas regalo",
184
+ giftCardRemaining: "Queda por pagar",
185
+ giftCardEnding: "Tarjeta regalo terminada en",
186
+
187
+ companyInvoice: "Factura de empresa",
188
+ companyName: "Nombre de la empresa",
189
+ companyVat: "NIF / CIF",
190
+ companyMol: "Persona responsable",
191
+ companyAddress: "Dirección de la empresa",
192
+
193
+ placeOrder: "Realizar pedido",
194
+ selectPaymentMethod: "Elige un método de pago",
195
+
196
+ processingCard: [
197
+ "Conectando con el banco...",
198
+ "Confirmando el pago...",
199
+ "Registrando el pedido...",
200
+ "Cuidando cada detalle...",
201
+ ],
202
+ processingCod: [
203
+ "Reservando la entrega...",
204
+ "Preparando el pedido...",
205
+ "Cuidando cada detalle...",
206
+ ],
207
+
208
+ econtLoadingOffices: "Cargando oficinas...",
209
+ econtNearestOffices: "Oficinas más cercanas",
210
+ econtSearchAnother: "Buscar otra oficina",
211
+ econtSearchPlaceholder: "Busca por nombre, ciudad o dirección...",
212
+ econtNoResults: "No se han encontrado oficinas para",
213
+ econtChange: "Cambiar",
214
+
215
+ boxnowLoadingLockers: "Cargando taquillas...",
216
+ boxnowNearestLockers: "Taquillas más cercanas",
217
+ boxnowSearchAnother: "Buscar otra taquilla",
218
+ boxnowSearchPlaceholder: "Busca por nombre, dirección o código postal...",
219
+ boxnowNoResults: "No se han encontrado taquillas para",
220
+ boxnowChange: "Cambiar",
221
+ boxnowUnavailable:
222
+ "BoxNow no está disponible temporalmente, elige otro método de envío.",
223
+ boxnowNoLockersInCity:
224
+ "No hay taquillas de BoxNow en tu ciudad. Elige otro método de envío.",
225
+ addressErrors: {
226
+ generic:
227
+ "No hemos podido guardar tu dirección. Revisa los datos e inténtalo de nuevo.",
228
+ network:
229
+ "No hay conexión con el servidor. Revisa tu conexión e inténtalo de nuevo.",
230
+ validationFailed:
231
+ "Alguno de los campos no es válido. Revisa los datos e inténtalo de nuevo.",
232
+ invalidRegion: "Ese país no es válido. Recarga la página.",
233
+ regionRequired: "La región de envío no es válida. Recarga la página.",
234
+ cartCompleted:
235
+ "El pedido ya está finalizado. Revisa tu correo para ver la confirmación.",
236
+ cartNotFound: "Tu sesión del carrito ha caducado. Recarga la página.",
237
+ invalidEmail: "Ese correo electrónico no es válido. Revisa cómo está escrito.",
238
+ invalidPostalCode: "Ese código postal no es válido. Revísalo.",
239
+ invalidPhone: "Ese número de teléfono no es válido. Revisa los dígitos.",
240
+ invalidCountry: "Ese país no es válido. Recarga la página.",
241
+ },
242
+ paymentErrors: {
243
+ genericCard:
244
+ "El pago no se ha podido procesar ahora mismo. Inténtalo de nuevo o elige otro método de pago.",
245
+ genericCod:
246
+ "El pedido no se ha podido completar ahora mismo. Inténtalo de nuevo en unos momentos.",
247
+ cartEmailRequired:
248
+ "Hace falta un correo electrónico para finalizar la compra. Añade uno para continuar.",
249
+ cartEmpty: "Tu carrito está vacío. Añade un producto para continuar.",
250
+ shippingAddressRequired:
251
+ "Hace falta una dirección de envío para finalizar la compra. Añade una para continuar.",
252
+ shippingMethodRequired: "Elige un método de envío para continuar.",
253
+ paymentCollectionRequired: "El pago aún no está listo. Inténtalo de nuevo.",
254
+ paymentSessionRequired: "El pago aún no está listo. Inténtalo de nuevo.",
255
+ insufficientInventory:
256
+ "Uno de los productos ya no está disponible en la cantidad elegida. Actualiza tu carrito para continuar.",
257
+ checkoutMethodHidden:
258
+ "El método elegido ya no está disponible para este pedido. Elige otro e inténtalo de nuevo.",
259
+ requiresAction:
260
+ "Tu banco necesita confirmar este pago. Sigue los pasos que te indique para continuar.",
261
+ paymentNotAuthorized:
262
+ "El pago no se ha aprobado. Inténtalo de nuevo o elige otro método de pago.",
263
+ paymentNotInitiated:
264
+ "El pago ha caducado antes de completarse. Recarga la página e inténtalo de nuevo.",
265
+ paymentIncomplete:
266
+ "Tus tarjetas regalo ya no cubren el importe total. Elige un método de pago para el resto.",
267
+ giftCardInsufficientBalance:
268
+ "El saldo de esa tarjeta regalo ya no cubre el importe. Retírala o elige otro método de pago.",
269
+ giftCardNotRedeemable:
270
+ "Esa tarjeta regalo ya no se puede usar. Retírala e inténtalo de nuevo.",
271
+ accountRequired:
272
+ "Para finalizar la compra tienes que iniciar sesión. Inicia sesión para continuar.",
273
+ cartLocked:
274
+ "Este pedido se está procesando ahora mismo. Espera unos segundos antes de volver a intentarlo.",
275
+ cartCompleted:
276
+ "Este pedido ya se ha completado. Revisa tu correo electrónico para ver la confirmación.",
277
+ cartNotFound:
278
+ "Tu carrito ya no está disponible. Recarga la página para empezar de nuevo.",
279
+ shippingOptionNotFound:
280
+ "El método de envío elegido ya no está disponible. Elige otro.",
281
+ invalidProvider: "El método de pago elegido no se puede usar. Elige otro.",
282
+ shippingPriceMissing:
283
+ "No se ha podido calcular el coste del envío. Elige otro método de envío.",
284
+ stripeNotConfigured:
285
+ "El pago con tarjeta no está disponible en este momento. Elige otro método de pago.",
286
+ validationFailed:
287
+ "Faltan algunos datos del pedido. Revisa el formulario e inténtalo de nuevo.",
288
+ cardDeclined:
289
+ "El banco ha rechazado esa tarjeta. Prueba con otra tarjeta o contacta con tu banco.",
290
+ insufficientFunds:
291
+ "Esa tarjeta no tiene saldo suficiente. Prueba con otra tarjeta.",
292
+ expiredCard: "Esa tarjeta ha caducado. Prueba con otra tarjeta.",
293
+ incorrectCvc:
294
+ "El código de seguridad no coincide. Revisa el código de seguridad de tu tarjeta e inténtalo de nuevo.",
295
+ incorrectCardNumber:
296
+ "El número de tarjeta no parece correcto. Revisa los dígitos e inténtalo de nuevo.",
297
+ invalidExpiry:
298
+ "La fecha de caducidad no parece correcta. Revisa el mes y el año.",
299
+ authenticationRequired:
300
+ "Tu banco necesita confirmar este pago. Sigue los pasos que te indique para continuar.",
301
+ processingError:
302
+ "No se ha podido procesar la tarjeta. Inténtalo de nuevo en unos momentos.",
303
+ rateLimited:
304
+ "Se han hecho demasiados intentos de pago. Espera unos minutos e inténtalo de nuevo.",
305
+ paymentExpired:
306
+ "El pago ha caducado antes de completarse. Recarga la página e inténtalo de nuevo.",
307
+ amountMismatch:
308
+ "El importe del pedido ha cambiado. Recarga la página e inténtalo de nuevo.",
309
+ networkError:
310
+ "Se ha perdido la conexión. Revisa tu conexión a internet e inténtalo de nuevo.",
311
+ cartAlreadyCompleted:
312
+ "Este pedido ya se ha completado. Revisa tu correo electrónico para ver la confirmación.",
313
+ outOfStock:
314
+ "Uno de los productos ya no está disponible en la cantidad elegida. Actualiza tu carrito para continuar.",
315
+ invalidRegion:
316
+ "Este destino de envío no está disponible. Recarga la página e inténtalo de nuevo.",
317
+ shippingMethodMissing: "Elige un método de envío para continuar.",
318
+ },
319
+ giftCardErrors: {
320
+ invalidGiftCard:
321
+ "Ese código de tarjeta regalo no es válido. Revisa el código e inténtalo de nuevo.",
322
+ rateLimited: "Demasiados intentos. Espera unos minutos e inténtalo de nuevo.",
323
+ cartCompleted:
324
+ "Este pedido ya está finalizado. Revisa tu correo para ver la confirmación.",
325
+ cartNotFound: "Tu carrito ha caducado. Recarga la página e inténtalo de nuevo.",
326
+ validationFailed: "Ese código no es válido. Revísalo e inténtalo de nuevo.",
327
+ generic:
328
+ "La tarjeta regalo no se ha podido aplicar ahora mismo. Inténtalo de nuevo en un momento.",
329
+ },
330
+ promotionErrors: {
331
+ generic:
332
+ "Este código de descuento no se puede aplicar ahora mismo. Revisa el código e inténtalo de nuevo.",
333
+ needsEmail:
334
+ "Este código de descuento necesita una dirección de correo electrónico. Añade la tuya arriba e inténtalo de nuevo.",
335
+ exhausted: "Esta promoción se ha agotado y ya no está disponible.",
336
+ promotionNotFound:
337
+ "Este código de descuento no es válido. Revísalo e inténtalo de nuevo.",
338
+ promotionInactive: "Este código de descuento ya no está activo.",
339
+ promotionMisconfigured:
340
+ "Este código de descuento no se puede aplicar ahora mismo. Revisa el código e inténtalo de nuevo.",
341
+ promotionUnsupportedType:
342
+ "Este código de descuento no se puede aplicar ahora mismo. Revisa el código e inténtalo de nuevo.",
343
+ cartCompleted:
344
+ "Este pedido ya se ha realizado. Revisa tu correo electrónico para ver la confirmación.",
345
+ cartNotFound:
346
+ "No encontramos tu carrito. Recarga la página e inténtalo de nuevo.",
347
+ validationFailed:
348
+ "Este código de descuento no es válido. Revísalo e inténtalo de nuevo.",
349
+ },
350
+ },
351
+ order: {
352
+ orderConfirmed: "Pedido confirmado",
353
+ confirmationSent: "Hemos enviado una confirmación a",
354
+ orderNumber: "Pedido",
355
+ orderDate: "Fecha",
356
+ summary: "Resumen del pedido",
357
+ subtotal: "Subtotal",
358
+ shipping: "Envío",
359
+ discount: "Descuento",
360
+ tax: "Impuestos",
361
+ total: "Total",
362
+ paymentMethodFee: "Comisión por método de pago",
363
+ contactInfo: "Datos de contacto",
364
+ delivery: "Entrega",
365
+ paymentMethod: "Método de pago",
366
+ paymentMethodTitles: {
367
+ card: "Tarjeta de crédito / débito",
368
+ cod: "Contra reembolso",
369
+ manual: "Pago manual",
370
+ paypal: "PayPal",
371
+ },
372
+ needHelp: "¿Necesitas ayuda?",
373
+ contactUs: "Contacta con nosotros",
374
+ returnsExchanges: "Devoluciones y cambios",
375
+ orderPlaced: "Realizado",
376
+ processing: "En proceso",
377
+ shipped: "Enviado",
378
+ delivered: "Entregado",
379
+ free: "Gratis",
380
+ continueShopping: "Seguir comprando",
381
+ qty: "cant.",
382
+ tracking: "Seguimiento",
383
+ },
384
+ reviews: {
385
+ sectionTitle: "Valoraciones",
386
+ reviewSingular: "valoración",
387
+ reviewPlural: "valoraciones",
388
+ sortAria: "Ordenar",
389
+ sortDefault: "Recomendadas",
390
+ sortNewest: "Más recientes",
391
+ sortRatingDesc: "Mejor valoradas",
392
+ sortRatingAsc: "Peor valoradas",
393
+ verified: "Compra verificada",
394
+ verifiedTitle: "Comprador real, verificado mediante un enlace de pedido",
395
+ loadMore: "Mostrar más",
396
+ loading: "Cargando...",
397
+ dateToday: "hoy",
398
+ dateYesterday: "ayer",
399
+ dateDaysAgo: "hace {n} días",
400
+ showPhoto: "Ver foto",
401
+ playVideo: "Reproducir vídeo",
402
+ close: "Cerrar",
403
+ expiredTitle: "Este enlace de valoración ha caducado",
404
+ expiredBody:
405
+ "El enlace era válido durante 60 días desde tu compra. Si quieres dejar una valoración, escríbenos a {email}.",
406
+ invalidTitle: "Este enlace de valoración no es válido",
407
+ invalidBody:
408
+ "Puede que hayas abierto un enlace antiguo. Si tienes alguna duda, escríbenos a {email}.",
409
+ alreadyTitle: "¡Gracias por tu valoración!",
410
+ alreadyBodyWithProduct: "Ya hemos recibido tu valoración de {name}.",
411
+ alreadyBody: "Esta valoración ya se ha enviado.",
412
+ greeting: "Hola",
413
+ headingRate: "Comparte tu opinión",
414
+ headingPhoto: "Solo queda un paso",
415
+ introRate:
416
+ "Una valoración breve con tus palabras ayuda a quienes están pensando en la misma elección.",
417
+ introPhoto:
418
+ "Tu puntuación está guardada. Añade una foto del producto y consigue un {pct}% de descuento en tu próximo pedido.",
419
+ productLabel: "Producto",
420
+ ratingLabel: "Puntuación",
421
+ ratingOutOf: "{n} de 5",
422
+ ratingAria: "{n} de 5 estrellas",
423
+ bodyLabel: "¿Qué te gustó y qué no?",
424
+ bodyPlaceholder:
425
+ "Descríbelo con tus palabras. Cuanto más detalle, más útil para el próximo cliente.",
426
+ consentNote:
427
+ "Al enviar la valoración aceptas que la publiquemos en la página del producto.",
428
+ continue: "Continuar",
429
+ submitting: "Enviando...",
430
+ bonusBadge: "Extra",
431
+ photoTitle: "Enséñalo y llévate un {pct}%",
432
+ photoSubtitle:
433
+ "Añade una foto o un vídeo corto del producto y te regalamos un {pct}% de descuento para tu próximo pedido.",
434
+ dropHint: "Arrastra los archivos aquí o haz clic para elegir",
435
+ dropLimits: "Hasta {n} fotos (JPG/PNG/WEBP, 8 MB), 1 vídeo (MP4/MOV, 50 MB, 60 s)",
436
+ skip: "Omitir",
437
+ skipAria: "Omitir",
438
+ submitPhoto: "Enviar y obtener el {pct}%",
439
+ waitingUpload: "Esperando la subida...",
440
+ remove: "Quitar",
441
+ thanksTitle: "¡Gracias!",
442
+ thanksBody: "Hemos recibido tu valoración y ya está publicada en la página del producto.",
443
+ rewardTitle: "Tu descuento del {pct}%",
444
+ copy: "Copiar",
445
+ copied: "Copiado",
446
+ copyAria: "Copiar el código",
447
+ rewardNote:
448
+ "También te hemos enviado el código por correo. Úsalo en tu próximo pedido: es válido para un solo uso.",
449
+ codeUnavailable:
450
+ "La foto se ha guardado. Algo ha fallado al generar tu código de descuento: escríbenos a {email} y te lo enviamos enseguida.",
451
+ viewProduct: "Ver producto",
452
+ toStore: "Volver a la tienda",
453
+ errUnsupported: "El archivo {name} no tiene un formato compatible.",
454
+ errMaxImages: "Máximo {n} fotos.",
455
+ errImageTooLarge: "La foto {name} supera los {mb} MB.",
456
+ errMaxVideos: "Máximo {n} vídeo.",
457
+ errVideoTooLarge: "El vídeo supera los {mb} MB.",
458
+ errVideoTooLong: "El vídeo dura más de {s} segundos.",
459
+ errVideoMeta: "No hemos podido leer la duración del vídeo.",
460
+ errUploadFailed: "No se ha podido subir {name}. Inténtalo de nuevo.",
461
+ errRateLimited: "Se han enviado demasiadas valoraciones desde esta dirección. Inténtalo más tarde.",
462
+ errAlreadySubmitted: "Esta valoración ya se ha enviado.",
463
+ errExpired: "El enlace de valoración ha caducado.",
464
+ errGeneric: "Algo ha fallado. Inténtalo de nuevo.",
465
+ errPhotoSave: "La foto no se ha podido guardar. Inténtalo de nuevo.",
466
+ },
467
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * THE LOCALE LAYER.
3
+ *
4
+ * The library speaks international ENGLISH by default and knows nothing
5
+ * about any other language. A language is a PACK the store imports:
6
+ *
7
+ * import { es } from "@cartbase/storefront/locales/es"
8
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
9
+ *
10
+ * Import a pack by its own subpath (`/locales/es`, `/locales/bg`) rather
11
+ * than from here, so a store that ships one language does not bundle the
12
+ * others. This barrel carries the type, the provider and English only.
13
+ *
14
+ * Shipping: English, Spanish, Bulgarian (2026-09-13). Adding a language is
15
+ * one file in this folder plus its export path, and nothing else.
16
+ */
17
+ export { en } from "./en"
18
+ export { StorefrontLocaleProvider, useStorefrontLocale } from "./provider"
19
+ export type { PropLabelAreas, StorefrontLocale } from "./types"
@@ -0,0 +1,63 @@
1
+ "use client"
2
+
3
+ import { createContext, useContext, type ReactNode } from "react"
4
+ import { CheckoutProvider } from "../checkout/context"
5
+ import { OrderLabelsProvider } from "../order/context"
6
+ import { ProductLabelsProvider } from "../products/context"
7
+ import type { StorefrontLocale } from "./types"
8
+ import { en } from "./en"
9
+
10
+ const StorefrontLocaleContext = createContext<StorefrontLocale>(en)
11
+
12
+ /**
13
+ * ONE PROVIDER FOR THE STORE'S LANGUAGE.
14
+ *
15
+ * import { es } from "@cartbase/storefront/locales/es"
16
+ * <StorefrontLocaleProvider locale={es}>{children}</StorefrontLocaleProvider>
17
+ *
18
+ * It mounts the three pure label contexts (products, checkout, order) so
19
+ * every component inside them is localized with no further wiring.
20
+ *
21
+ * IT DOES NOT MOUNT THE CART DRAWER, and that is deliberate rather than an
22
+ * omission: `CartDrawerProvider` carries the cart's state, its client and
23
+ * its persistence callbacks, so it belongs where the app owns that state,
24
+ * not inside a language wrapper. Hand it the pack instead:
25
+ *
26
+ * const locale = useStorefrontLocale()
27
+ * <CartDrawerProvider cart={cart} labels={locale.cartDrawer}>
28
+ *
29
+ * Same for the two areas that read labels from a prop:
30
+ *
31
+ * <StoreTemplate labels={locale.store} />
32
+ * <ReviewWidget labels={locale.reviews} />
33
+ *
34
+ * DEFAULT IS ENGLISH. A component rendered outside this provider reads the
35
+ * library's own English defaults, so nothing renders blank or crashes for
36
+ * want of a provider.
37
+ */
38
+ export function StorefrontLocaleProvider({
39
+ locale,
40
+ children,
41
+ }: {
42
+ locale: StorefrontLocale
43
+ children: ReactNode
44
+ }) {
45
+ return (
46
+ <StorefrontLocaleContext.Provider value={locale}>
47
+ <ProductLabelsProvider labels={locale.products}>
48
+ <CheckoutProvider labels={locale.checkout}>
49
+ <OrderLabelsProvider labels={locale.order}>{children}</OrderLabelsProvider>
50
+ </CheckoutProvider>
51
+ </ProductLabelsProvider>
52
+ </StorefrontLocaleContext.Provider>
53
+ )
54
+ }
55
+
56
+ /**
57
+ * The whole locale, for the packs this provider cannot mount (the cart
58
+ * drawer's, the store template's and the reviews widget's) and for the
59
+ * code that needs the language itself, e.g. `<html lang={locale.code}>`.
60
+ */
61
+ export function useStorefrontLocale(): StorefrontLocale {
62
+ return useContext(StorefrontLocaleContext)
63
+ }
@@ -0,0 +1,49 @@
1
+ import type { CartDrawerLabels } from "../cart-drawer/labels"
2
+ import type { CheckoutLabels } from "../checkout/labels"
3
+ import type { OrderLabels } from "../order/labels"
4
+ import type { ProductLabels } from "../products/labels"
5
+ import type { ReviewsUiLabels } from "../reviews-ui/labels"
6
+ import type { StoreLabels } from "../store/labels"
7
+
8
+ /**
9
+ * A LOCALE IS ONE OBJECT COVERING THE WHOLE STOREFRONT.
10
+ *
11
+ * Before this (2026-09-13) every area shipped its own English defaults and
12
+ * its own Bulgarian file, so localizing a store meant importing six packs
13
+ * and mounting four separate providers, and a global package shipped
14
+ * Bulgarian copy to every merchant on earth. Alexander's ruling: the
15
+ * library is international ENGLISH, and a language is a pack the store
16
+ * opts into.
17
+ *
18
+ * So: English is the library's own default and lives in each area's
19
+ * `labels.ts` beside its type. Every other language is a locale pack
20
+ * imported by subpath — `@cartbase/storefront/locales/es`,
21
+ * `/locales/bg` — and is typed against these six shapes, which means a
22
+ * missing translation is a COMPILE ERROR rather than a silent fall back to
23
+ * English in the middle of a Spanish checkout.
24
+ *
25
+ * Adding a language is one file in `src/locales/` plus its export path.
26
+ * Nothing else in the package changes, and no component knows a language
27
+ * exists.
28
+ */
29
+ export interface StorefrontLocale {
30
+ /** BCP 47 code, for the app's own persistence and for `<html lang>`. */
31
+ code: string
32
+ /** The language's own name, for a switcher that does not want Intl. */
33
+ name: string
34
+ products: ProductLabels
35
+ store: StoreLabels
36
+ cartDrawer: CartDrawerLabels
37
+ checkout: CheckoutLabels
38
+ order: OrderLabels
39
+ reviews: ReviewsUiLabels
40
+ }
41
+
42
+ /**
43
+ * The two areas that read labels from a PROP rather than a context
44
+ * (`store` and `reviews`) are handed their pack by the app:
45
+ * `<StoreTemplate labels={locale.store} />`. The other four are served by
46
+ * `StorefrontLocaleProvider`. That split is the components' existing
47
+ * shape, not a decision of this layer.
48
+ */
49
+ export type PropLabelAreas = Pick<StorefrontLocale, "store" | "reviews">
@@ -5,7 +5,6 @@
5
5
  */
6
6
  export { OrderLabelsProvider, useOrderLabels } from "./context"
7
7
  export { defaultOrderLabels, type OrderLabels } from "./labels"
8
- export { bulgarianOrderLabels } from "./labels-bg"
9
8
 
10
9
  export {
11
10
  OrderConfirmationHeader,
@@ -27,10 +27,19 @@ export type SelectFieldProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
27
27
  label: string
28
28
  children: React.ReactNode
29
29
  className?: string
30
+ /**
31
+ * A mark shown inside the control, before the value: a flag, a currency
32
+ * symbol. Decorative and click-through, so the whole field still opens the
33
+ * select. The value and the floating label make room for it.
34
+ */
35
+ leading?: React.ReactNode
30
36
  }
31
37
 
32
38
  export const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>(
33
- function SelectField({ label, required, disabled, children, className, value, ...props }, ref) {
39
+ function SelectField(
40
+ { label, required, disabled, children, className, value, leading, ...props },
41
+ ref
42
+ ) {
34
43
  const hasValue = value !== undefined && value !== null && value !== ""
35
44
 
36
45
  return (
@@ -41,7 +50,8 @@ export const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>
41
50
  disabled={disabled}
42
51
  value={value}
43
52
  className={cn(
44
- "w-full h-[44px] px-3 pt-[14px] pb-[2px] pr-9 text-sm rounded-lg border transition-colors duration-150 appearance-none outline-none",
53
+ "w-full h-[44px] pt-[14px] pb-[2px] pr-9 text-sm rounded-lg border transition-colors duration-150 appearance-none outline-none",
54
+ leading ? "pl-10" : "px-3",
45
55
  "focus:border-primary focus:bg-primary/5",
46
56
  disabled
47
57
  ? "bg-muted border-border text-muted-foreground cursor-not-allowed"
@@ -51,9 +61,15 @@ export const SelectField = React.forwardRef<HTMLSelectElement, SelectFieldProps>
51
61
  >
52
62
  {children}
53
63
  </select>
64
+ {leading && (
65
+ <span className="absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none flex items-center">
66
+ {leading}
67
+ </span>
68
+ )}
54
69
  <span
55
70
  className={cn(
56
- "absolute pointer-events-none left-3 top-[14px] text-sm leading-4 origin-top-left",
71
+ "absolute pointer-events-none top-[14px] text-sm leading-4 origin-top-left",
72
+ leading ? "left-10" : "left-3",
57
73
  "transition-transform transition-colors duration-150 ease-out",
58
74
  hasValue ? "text-muted-foreground" : "text-muted-foreground",
59
75
  hasValue ? "-translate-y-2 scale-[0.77]" : "translate-y-0 scale-100"
@@ -5,7 +5,6 @@
5
5
  */
6
6
  export { ProductLabelsProvider, useProductLabels } from "./context"
7
7
  export { defaultProductLabels, type ProductLabels } from "./labels"
8
- export { bulgarianProductLabels } from "./labels-bg"
9
8
 
10
9
  export {
11
10
  optionsAsKeymap,
@@ -10,7 +10,6 @@ export {
10
10
  defaultReviewsUiLabels,
11
11
  type ReviewsUiLabels,
12
12
  } from "./labels"
13
- export { bulgarianReviewsUiLabels } from "./labels-bg"
14
13
 
15
14
  export {
16
15
  formatLabel,