orionplace-nuxt 0.7.5 → 0.7.7
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.
- package/README.md +6 -4
- package/package.json +2 -2
- package/runtime/marketplace.mjs +83 -5
- package/src/project-readme.mjs +2 -2
- package/templates/app/components/orionplace/search/MainSearch.vue +3 -5
- package/templates/app/components/orionplace/storefront/StorefrontAccount.README.md +1 -1
- package/templates/app/components/orionplace/storefront/StorefrontFooter.vue +3 -1
- package/templates/app/composables/useOrionplaceApi.ts +14 -1
- package/templates/app/utils/orionplace/accountLabels.ts +7 -0
- package/templates/app/utils/orionplace/activityLabels.ts +32 -1
- package/templates/app/utils/orionplace/blogLabels.ts +53 -1
- package/templates/app/utils/orionplace/breadcrumbLabels.ts +4 -1
- package/templates/app/utils/orionplace/browseLabels.ts +48 -1
- package/templates/app/utils/orionplace/cartLabels.ts +128 -1
- package/templates/app/utils/orionplace/cartPreview.ts +15 -0
- package/templates/app/utils/orionplace/categoryLabels.ts +14 -1
- package/templates/app/utils/orionplace/contactLabels.ts +51 -1
- package/templates/app/utils/orionplace/directoryLabels.ts +66 -1
- package/templates/app/utils/orionplace/errorPageLabels.ts +15 -0
- package/templates/app/utils/orionplace/featureLabels.ts +136 -1
- package/templates/app/utils/orionplace/featuredLabels.ts +10 -0
- package/templates/app/utils/orionplace/footerLabels.ts +17 -1
- package/templates/app/utils/orionplace/headerLabels.ts +30 -0
- package/templates/app/utils/orionplace/homeLabels.ts +22 -0
- package/templates/app/utils/orionplace/manufacturerIndex.ts +17 -0
- package/templates/app/utils/orionplace/orderLabels.ts +146 -1
- package/templates/app/utils/orionplace/privacyLabels.ts +77 -2
- package/templates/app/utils/orionplace/productActionLabels.ts +27 -0
- package/templates/app/utils/orionplace/productLabels.ts +37 -0
- package/templates/app/utils/orionplace/profileLabels.ts +77 -1
- package/templates/app/utils/orionplace/promoLabels.ts +37 -1
- package/templates/app/utils/orionplace/reviewLabels.ts +81 -1
- package/templates/app/utils/orionplace/sessionLabels.ts +12 -1
- package/templates/app/utils/orionplace/subscriptionLabels.ts +34 -1
- package/templates/app/utils/orionplace/subscriptionManageLabels.ts +119 -2
- package/templates/app/utils/orionplace/tariffLabels.ts +55 -5
- package/templates/app/utils/orionplace/tariffVariables.ts +38 -0
- package/templates/app/utils/orionplace/ticketLabels.ts +94 -1
- package/templates/orionplace/COMPONENT-DOWNLOAD.md +1 -1
- package/templates/orionplace/COMPONENTS.md +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`orionplace-nuxt` creates a server-rendered shop connected to your personal
|
|
6
6
|
marketplace API: catalog and search, product pages, cart and checkout, customer
|
|
7
|
-
account, journal and support, in English and
|
|
7
|
+
account, journal and support, in English, Russian and Spanish. The package includes a Nuxt module, a
|
|
8
8
|
setup CLI and the full storefront source, so every page and component in your
|
|
9
9
|
project is yours to change.
|
|
10
10
|
|
|
@@ -468,7 +468,9 @@ authorization and cookies are forwarded. Backend statuses are retained and cooki
|
|
|
468
468
|
Domain attributes are removed to bind cookies to the storefront. Gateway responses
|
|
469
469
|
use `Cache-Control: no-store`, except public blog images and videos: they keep the
|
|
470
470
|
backend's `private, max-age` rule, so the browser does not download them again on
|
|
471
|
-
every page.
|
|
471
|
+
every page. When the network drops the connection before the backend answers, GET,
|
|
472
|
+
HEAD and Application Load requests are sent once more on a new connection; other
|
|
473
|
+
requests are never repeated. Network failures return `502`.
|
|
472
474
|
|
|
473
475
|
A changing request (POST, PUT, PATCH, DELETE) from another site is rejected with
|
|
474
476
|
`403`. A request from the page's own host or the storefront domain passes, and the
|
|
@@ -584,7 +586,7 @@ is shared by selectors, product prices, wallet balances, cart totals and tariffs
|
|
|
584
586
|
it does not convert or round amounts.
|
|
585
587
|
Its home link uses `BrandLogos` with `store.init.logos.medium`; contact data is
|
|
586
588
|
not a header dependency. Missing or failed logo images use the shared placeholder.
|
|
587
|
-
Its labels follow the storefront locale (English/Russian). `StorefrontAccount`
|
|
589
|
+
Its labels follow the storefront locale (English/Russian/Spanish). `StorefrontAccount`
|
|
588
590
|
shows guest sign-in or an authenticated profile menu with the selected currency
|
|
589
591
|
wallet balance. Its `signIn`, `signOut`, `selectProfile` and `selectWallet` events are handled
|
|
590
592
|
by the host; the generated layout opens `/profile` for sign-in (with the current
|
|
@@ -1167,5 +1169,5 @@ purchased subscriptions and disabling automatic renewal. Choosing a new renewal
|
|
|
1167
1169
|
|
|
1168
1170
|
See [`templates/orionplace/COMPONENTS.md`](templates/orionplace/COMPONENTS.md)
|
|
1169
1171
|
for the maintained route/component/endpoint table. Components use the named
|
|
1170
|
-
API client, confirmed mutation results and English/Russian labels. Customer
|
|
1172
|
+
API client, confirmed mutation results and English/Russian/Spanish labels. Customer
|
|
1171
1173
|
source bundles include transitive dependencies and installation guidance.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orionplace-nuxt",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Nuxt 4 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English and
|
|
3
|
+
"version": "0.7.7",
|
|
4
|
+
"description": "Nuxt 4 storefront for your Orionplace marketplace: setup CLI, server API gateway, typed store and ready-made catalog, cart, checkout and account components in English, Russian and Spanish, plus an Android app build.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Orionplace",
|
package/runtime/marketplace.mjs
CHANGED
|
@@ -142,6 +142,74 @@ export function marketplaceConfigDirectory(root = process.cwd()) {
|
|
|
142
142
|
: resolve(root, 'sitemaps');
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// The network can drop a pooled connection to the backend (a reset somewhere
|
|
146
|
+
// between this server and the API host). The request then fails before any
|
|
147
|
+
// answer arrives, so a request that is safe to repeat gets one more try on a
|
|
148
|
+
// new connection instead of a failed page.
|
|
149
|
+
const droppedConnection = new Set([
|
|
150
|
+
'ECONNRESET',
|
|
151
|
+
'ECONNABORTED',
|
|
152
|
+
'EPIPE',
|
|
153
|
+
'UND_ERR_SOCKET',
|
|
154
|
+
'UND_ERR_CLOSED',
|
|
155
|
+
]);
|
|
156
|
+
// Application Load only reads, and its body is a small JSON object; a larger
|
|
157
|
+
// body is streamed once without a retry.
|
|
158
|
+
const appLoadPath = /^\/api\/appload\/?$/;
|
|
159
|
+
const repeatBodyLimit = 64 * 1024;
|
|
160
|
+
|
|
161
|
+
/** @param {any} error */
|
|
162
|
+
function connectionDropped(error) {
|
|
163
|
+
return droppedConnection.has(error?.cause?.code ?? error?.code);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The upstream body and whether the request may be sent twice: GET and HEAD
|
|
168
|
+
* have no body; Application Load keeps its small body for a second try.
|
|
169
|
+
* @param {Request} request @param {string} pathname
|
|
170
|
+
* @returns {Promise<{body: any, repeatable: boolean}>}
|
|
171
|
+
*/
|
|
172
|
+
async function upstreamBody(request, pathname) {
|
|
173
|
+
if (['GET', 'HEAD'].includes(request.method)) {
|
|
174
|
+
return { body: undefined, repeatable: true };
|
|
175
|
+
}
|
|
176
|
+
if (!request.body || !appLoadPath.test(pathname)) {
|
|
177
|
+
return { body: request.body, repeatable: false };
|
|
178
|
+
}
|
|
179
|
+
const reader = request.body.getReader();
|
|
180
|
+
const chunks = [];
|
|
181
|
+
let size = 0;
|
|
182
|
+
for (;;) {
|
|
183
|
+
const { done, value } = await reader.read();
|
|
184
|
+
if (done) {
|
|
185
|
+
return { body: Buffer.concat(chunks), repeatable: true };
|
|
186
|
+
}
|
|
187
|
+
chunks.push(value);
|
|
188
|
+
size += value.byteLength;
|
|
189
|
+
if (size > repeatBodyLimit) {
|
|
190
|
+
const body = new ReadableStream({
|
|
191
|
+
start(controller) {
|
|
192
|
+
for (const chunk of chunks) {
|
|
193
|
+
controller.enqueue(chunk);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
async pull(controller) {
|
|
197
|
+
const next = await reader.read();
|
|
198
|
+
if (next.done) {
|
|
199
|
+
controller.close();
|
|
200
|
+
} else {
|
|
201
|
+
controller.enqueue(next.value);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
cancel(reason) {
|
|
205
|
+
return reader.cancel(reason);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return { body, repeatable: false };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
145
213
|
/** @param {number} status @param {string} message */
|
|
146
214
|
function failure(status, message) {
|
|
147
215
|
return Response.json(
|
|
@@ -271,13 +339,23 @@ export async function forwardMarketplace(request, configuration = {}) {
|
|
|
271
339
|
]),
|
|
272
340
|
duplex: 'half',
|
|
273
341
|
};
|
|
274
|
-
if (!['GET', 'HEAD'].includes(request.method)) {
|
|
275
|
-
options.body = request.body;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
342
|
let upstream;
|
|
279
343
|
try {
|
|
280
|
-
|
|
344
|
+
const { body, repeatable } = await upstreamBody(request, incoming.pathname);
|
|
345
|
+
if (body !== undefined) {
|
|
346
|
+
options.body = body;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
upstream = await fetch(target, options);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!repeatable || !connectionDropped(error) || options.signal.aborted) {
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
console.warn(
|
|
355
|
+
`[orionplace] Backend connection dropped (${error?.cause?.code ?? error?.code}), retrying once`,
|
|
356
|
+
);
|
|
357
|
+
upstream = await fetch(target, options);
|
|
358
|
+
}
|
|
281
359
|
} catch (error) {
|
|
282
360
|
// The browser receives only the status; keep the cause in the server log.
|
|
283
361
|
if (!request.signal.aborted) {
|
package/src/project-readme.mjs
CHANGED
|
@@ -445,7 +445,7 @@ children, with live previews and source downloads. Russian guides are available
|
|
|
445
445
|
The default layout renders \`StorefrontHeader\` above every page, including the
|
|
446
446
|
StorefrontHome sections. It combines catalog navigation with Manufacturers and
|
|
447
447
|
Journal links, search, language/currency selection, the account menu and
|
|
448
|
-
wishlist/cart buttons. English and
|
|
448
|
+
wishlist/cart buttons. English, Russian and Spanish labels
|
|
449
449
|
follow the shared storefront locale.
|
|
450
450
|
\`StorefrontAccount\` adds guest sign-in and an authenticated profile/wallet menu.
|
|
451
451
|
The generated layout opens \`/profile\` for sign-in and profile selection;
|
|
@@ -690,7 +690,7 @@ account lists dates and can disable automatic renewal. Choosing a new renewal pe
|
|
|
690
690
|
endpoint names. API requests use the discovered method and URL, the current
|
|
691
691
|
session, CSRF and confirmed response handling. Account sections mount after
|
|
692
692
|
sign-in. Failed reads preserve existing content; form errors preserve drafts.
|
|
693
|
-
Physical components, their composables, styles and English/Russian labels are
|
|
693
|
+
Physical components, their composables, styles and English/Russian/Spanish labels are
|
|
694
694
|
installed from this package. Source ZIPs include their imported dependencies;
|
|
695
695
|
see [COMPONENT-DOWNLOAD.md](COMPONENT-DOWNLOAD.md) for consumer configuration.
|
|
696
696
|
|
|
@@ -199,14 +199,12 @@
|
|
|
199
199
|
props.historyPending || props.history.length || props.historyError,
|
|
200
200
|
),
|
|
201
201
|
);
|
|
202
|
-
const
|
|
203
|
-
selectionStore.localeCode.toLowerCase().startsWith('ru'),
|
|
204
|
-
);
|
|
202
|
+
const language = computed(() => selectionStore.localeCode.toLowerCase().slice(0, 2));
|
|
205
203
|
const loadingLabel = computed(() =>
|
|
206
|
-
|
|
204
|
+
({ ru: 'Загрузка…', es: 'Cargando…' } as Record<string, string>)[language.value] ?? 'Loading…',
|
|
207
205
|
);
|
|
208
206
|
const removeLabel = computed(() =>
|
|
209
|
-
|
|
207
|
+
({ ru: 'Удалить из истории', es: 'Quitar del historial' } as Record<string, string>)[language.value] ?? 'Remove from history',
|
|
210
208
|
);
|
|
211
209
|
|
|
212
210
|
// The history follows the query while its list is open.
|
|
@@ -31,7 +31,7 @@ The authenticated name comes from `store.profile`; missing names use Profile.
|
|
|
31
31
|
The wallet reads only the selected currency's backend-converted amount. Zero
|
|
32
32
|
stays zero, missing/non-finite amounts show a dash. `WalletBalance` supports
|
|
33
33
|
`compact` (default `false`) and a `label` prop. Account labels follow the shared
|
|
34
|
-
storefront locale (English/Russian) and colors inherit the shared theme.
|
|
34
|
+
storefront locale (English/Russian/Spanish) and colors inherit the shared theme.
|
|
35
35
|
|
|
36
36
|
The documentation preview uses an isolated init snapshot, excludes private
|
|
37
37
|
profile data and records events without authenticating the reader. Header
|
|
@@ -138,7 +138,9 @@
|
|
|
138
138
|
catalog: copy.catalog,
|
|
139
139
|
tariffs: selectionStore.localeCode.startsWith('ru')
|
|
140
140
|
? 'Тарифы подписок'
|
|
141
|
-
: '
|
|
141
|
+
: selectionStore.localeCode.startsWith('es')
|
|
142
|
+
? 'Planes de suscripción'
|
|
143
|
+
: 'Subscription plans',
|
|
142
144
|
manufacturers: copy.manufacturers,
|
|
143
145
|
...(store.init?.visual_settings?.single_seller_mode ? {} : { sellers: copy.sellers }),
|
|
144
146
|
stores: copy.stores,
|
|
@@ -73,8 +73,21 @@ function createClients(): OrionplaceClients {
|
|
|
73
73
|
return nativeAppRequest(appOrigin, path, request);
|
|
74
74
|
}
|
|
75
75
|
let received: OrionplaceApiResponse | undefined;
|
|
76
|
+
// A server render's own API call copies the visitor's request headers.
|
|
77
|
+
// A visitor who follows a link from another site arrives with
|
|
78
|
+
// `Sec-Fetch-Site: cross-site` (and a form post with a foreign Origin);
|
|
79
|
+
// the gateway's cross-site check would refuse the render's call and the
|
|
80
|
+
// page would answer 503. The call comes from this server, so it is sent
|
|
81
|
+
// as same-origin without the visitor's Origin.
|
|
76
82
|
const headers = jar
|
|
77
|
-
? {
|
|
83
|
+
? {
|
|
84
|
+
...request.headers,
|
|
85
|
+
cookie: jar.header(),
|
|
86
|
+
origin: '',
|
|
87
|
+
'sec-fetch-site': 'same-origin',
|
|
88
|
+
'sec-fetch-mode': 'cors',
|
|
89
|
+
'sec-fetch-dest': 'empty',
|
|
90
|
+
}
|
|
78
91
|
: request.headers;
|
|
79
92
|
|
|
80
93
|
await requestFetch<unknown>(path, {
|
|
@@ -15,6 +15,13 @@ const labels = {
|
|
|
15
15
|
wallet: 'Кошелёк',
|
|
16
16
|
account: 'Личный кабинет',
|
|
17
17
|
},
|
|
18
|
+
es: {
|
|
19
|
+
signIn: 'Iniciar sesión',
|
|
20
|
+
signOut: 'Cerrar sesión',
|
|
21
|
+
profile: 'Perfil',
|
|
22
|
+
wallet: 'Monedero',
|
|
23
|
+
account: 'Tu cuenta',
|
|
24
|
+
},
|
|
18
25
|
};
|
|
19
26
|
|
|
20
27
|
export function accountLabels(locale: string) {
|
|
@@ -60,6 +60,37 @@ const ru: typeof en = {
|
|
|
60
60
|
viewProduct: 'Открыть товар',
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
const es: typeof en = {
|
|
64
|
+
title: 'Mi actividad',
|
|
65
|
+
logs: 'Historial de accesos',
|
|
66
|
+
logsTab: 'Accesos',
|
|
67
|
+
logsEmpty: 'No hay accesos que mostrar.',
|
|
68
|
+
from: 'Desde la fecha',
|
|
69
|
+
to: 'Hasta la fecha',
|
|
70
|
+
device: 'Dispositivo',
|
|
71
|
+
browser: 'Navegador',
|
|
72
|
+
location: 'Ubicación',
|
|
73
|
+
unknown: 'Desconocido',
|
|
74
|
+
promocodes: 'Mis códigos promocionales',
|
|
75
|
+
promocodesTab: 'Promociones',
|
|
76
|
+
promosEmpty: 'No hay códigos promocionales disponibles.',
|
|
77
|
+
apply: 'Aplicar',
|
|
78
|
+
applied: 'Aplicado al carrito',
|
|
79
|
+
notStarted: 'Empieza el',
|
|
80
|
+
expired: 'Caducado',
|
|
81
|
+
unavailable: 'No disponible',
|
|
82
|
+
cart: 'Abrir el carrito',
|
|
83
|
+
reviews: 'Mis reseñas',
|
|
84
|
+
reviewsTab: 'Reseñas',
|
|
85
|
+
wishlist: 'Favoritos',
|
|
86
|
+
wishlistEmpty: 'Guarda productos con el botón del corazón para encontrarlos aquí.',
|
|
87
|
+
notifications: 'Avisos',
|
|
88
|
+
notificationsEmpty: 'No hay productos con avisos de disponibilidad.',
|
|
89
|
+
removeWish: 'Quitar',
|
|
90
|
+
stopNotification: 'Cancelar aviso',
|
|
91
|
+
viewProduct: 'Ver el producto',
|
|
92
|
+
};
|
|
93
|
+
|
|
63
94
|
export function activityLabels(locale: string) {
|
|
64
|
-
return locale.startsWith('ru') ? ru : en;
|
|
95
|
+
return locale.startsWith('ru') ? ru : locale.startsWith('es') ? es : en;
|
|
65
96
|
}
|
|
@@ -110,6 +110,58 @@ const ru: typeof en = {
|
|
|
110
110
|
imageViewer: 'Просмотр изображения',
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
const es: typeof en = {
|
|
114
|
+
title: 'Blog',
|
|
115
|
+
category: 'Categoría',
|
|
116
|
+
writer: 'Autor',
|
|
117
|
+
allCategories: 'Todas las categorías',
|
|
118
|
+
allWriters: 'Todos los autores',
|
|
119
|
+
comments: 'Comentarios',
|
|
120
|
+
mine: 'Mías',
|
|
121
|
+
everyone: 'Debate',
|
|
122
|
+
message: 'Comentario',
|
|
123
|
+
send: 'Publicar',
|
|
124
|
+
reply: 'Responder',
|
|
125
|
+
replies: 'Respuestas',
|
|
126
|
+
replyTo: 'Responder a',
|
|
127
|
+
own: 'Tu comentario',
|
|
128
|
+
more: 'Más',
|
|
129
|
+
moreReplies: 'Ver más respuestas',
|
|
130
|
+
writeComment: 'Escribir un comentario',
|
|
131
|
+
editing: 'Editar el comentario',
|
|
132
|
+
cancel: 'Cancelar',
|
|
133
|
+
empty: 'Sé el primero en comentar.',
|
|
134
|
+
noMine: 'Todavía no tienes comentarios en este artículo.',
|
|
135
|
+
pending: 'Pendiente de publicación',
|
|
136
|
+
denied: 'No publicada',
|
|
137
|
+
deleteQuestion: '¿Eliminar este comentario?',
|
|
138
|
+
published: 'Publicado',
|
|
139
|
+
minutes: 'min de lectura',
|
|
140
|
+
fullAccess: 'Sigue leyendo con una suscripción',
|
|
141
|
+
fullAccessText:
|
|
142
|
+
'Este es el comienzo del artículo. El texto completo se abre con un plan de suscripción activo que incluya este artículo.',
|
|
143
|
+
tariffs: 'Planes',
|
|
144
|
+
tariffsHint: 'Planes de suscripción',
|
|
145
|
+
related: 'Productos de este artículo',
|
|
146
|
+
unavailable: 'Este artículo no está disponible.',
|
|
147
|
+
accessChanged: 'El acceso al artículo ha cambiado. Actualiza para comprobarlo.',
|
|
148
|
+
back: 'Artículos',
|
|
149
|
+
backHint: 'Todos los artículos',
|
|
150
|
+
video: 'Vídeo del artículo',
|
|
151
|
+
filters: 'Filtros',
|
|
152
|
+
filtersHint: 'Mostrar u ocultar los filtros de artículos',
|
|
153
|
+
categories: 'Categorías',
|
|
154
|
+
writers: 'Autores',
|
|
155
|
+
reset: 'Restablecer',
|
|
156
|
+
resetHint: 'Restablecer los filtros de artículos',
|
|
157
|
+
articleCount: (count: number) =>
|
|
158
|
+
`${count} ${count === 1 ? 'artículo' : 'artículos'}`,
|
|
159
|
+
subscription: 'Suscripción',
|
|
160
|
+
noFiltered: 'Ningún artículo coincide con los filtros seleccionados.',
|
|
161
|
+
imageOpen: 'Abrir la imagen',
|
|
162
|
+
imageViewer: 'Visor de imágenes',
|
|
163
|
+
};
|
|
164
|
+
|
|
113
165
|
export function blogLabels(locale: string) {
|
|
114
|
-
return locale.toLowerCase().startsWith('ru') ? ru : en;
|
|
166
|
+
return locale.toLowerCase().startsWith('ru') ? ru : locale.toLowerCase().startsWith('es') ? es : en;
|
|
115
167
|
}
|
|
@@ -47,7 +47,54 @@ export function browseLabels(locale: string) {
|
|
|
47
47
|
query: 'Название товара (от 3 символов)',
|
|
48
48
|
home: 'Главная',
|
|
49
49
|
}
|
|
50
|
-
: {
|
|
50
|
+
: locale.toLowerCase().startsWith('es') ? {
|
|
51
|
+
price: 'Precio',
|
|
52
|
+
from: 'Desde',
|
|
53
|
+
to: 'Hasta',
|
|
54
|
+
priceFrom: 'Precio mínimo',
|
|
55
|
+
priceTo: 'Precio máximo',
|
|
56
|
+
applyPrice: 'Aplicar',
|
|
57
|
+
moreFilters: 'Más filtros',
|
|
58
|
+
discountFrom: 'Descuento mínimo, %',
|
|
59
|
+
ratedProducts: 'Productos con valoración de 4,5 o más',
|
|
60
|
+
ratedSellers: 'Vendedores con valoración de 4,5 o más',
|
|
61
|
+
withReviews: 'Solo con reseñas',
|
|
62
|
+
results: 'Productos encontrados',
|
|
63
|
+
products: 'Productos',
|
|
64
|
+
searchResults: 'Resultados de búsqueda',
|
|
65
|
+
clear: 'Borrar',
|
|
66
|
+
emptyTitle: 'No se han encontrado productos',
|
|
67
|
+
emptyHint: 'Prueba otra búsqueda o borra los filtros.',
|
|
68
|
+
emptyCategoryHint:
|
|
69
|
+
'Todavía no hay productos que coincidan en esta categoría. Prueba a quitar filtros o elige otra categoría.',
|
|
70
|
+
startTitle: '¿Qué estás buscando?',
|
|
71
|
+
startHint:
|
|
72
|
+
'Escribe el nombre de un producto en la búsqueda: al menos tres caracteres.',
|
|
73
|
+
page: 'Página',
|
|
74
|
+
of: 'de',
|
|
75
|
+
categories: 'Categorías',
|
|
76
|
+
sellers: 'Vendedores',
|
|
77
|
+
manufacturers: 'Fabricantes',
|
|
78
|
+
stores: 'Tiendas',
|
|
79
|
+
searchCrumb: 'Buscar',
|
|
80
|
+
breadcrumb: 'Ruta de la página',
|
|
81
|
+
manufacturerProducts: 'Productos del fabricante',
|
|
82
|
+
sellerProducts: 'Productos del vendedor',
|
|
83
|
+
storeProducts: 'Productos disponibles en esta tienda',
|
|
84
|
+
filters: 'Filtros',
|
|
85
|
+
emptyFilters: 'No hay filtros disponibles',
|
|
86
|
+
blog: 'Blog',
|
|
87
|
+
emptyBlog: 'Aún no hay publicaciones',
|
|
88
|
+
minutes: 'min de lectura',
|
|
89
|
+
loading: 'Cargando…',
|
|
90
|
+
failed: 'No se han podido cargar los datos',
|
|
91
|
+
retry: 'Reintentar',
|
|
92
|
+
previous: 'Anterior',
|
|
93
|
+
next: 'Siguiente',
|
|
94
|
+
search: 'Buscar',
|
|
95
|
+
query: 'Nombre del producto (al menos 3 caracteres)',
|
|
96
|
+
home: 'Inicio',
|
|
97
|
+
} : {
|
|
51
98
|
price: 'Price',
|
|
52
99
|
from: 'From',
|
|
53
100
|
to: 'To',
|
|
@@ -250,6 +250,133 @@ const ru: typeof en = {
|
|
|
250
250
|
'Не удалось подтвердить создание заказа. Перед повторной попыткой проверьте историю заказов.',
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
+
const es: typeof en = {
|
|
254
|
+
title: 'Carrito',
|
|
255
|
+
checkout: 'Finalizar compra',
|
|
256
|
+
checkoutAction: 'Finalizar compra',
|
|
257
|
+
items: 'Artículos',
|
|
258
|
+
quantity: 'Cantidad',
|
|
259
|
+
empty: 'Tu carrito está vacío.',
|
|
260
|
+
browse: 'Seguir comprando',
|
|
261
|
+
remove: 'Quitar',
|
|
262
|
+
clear: 'Borrar',
|
|
263
|
+
clearQuestion: '¿Quitar todos los artículos del carrito?',
|
|
264
|
+
unavailable: 'No disponible para este pedido',
|
|
265
|
+
address: 'Dirección de entrega',
|
|
266
|
+
addAddress: 'Añadir',
|
|
267
|
+
chooseAddress: 'Elige una dirección guardada',
|
|
268
|
+
selectedAddress: 'Entrega en',
|
|
269
|
+
unsavedAddress:
|
|
270
|
+
'Esta dirección no está entre tus direcciones guardadas. Elige una dirección guardada para sustituirla.',
|
|
271
|
+
abroadHidden:
|
|
272
|
+
'No se muestran las direcciones guardadas en países sin envío.',
|
|
273
|
+
savedAbroad:
|
|
274
|
+
'La dirección está guardada, pero no hay envío a este país. Elige otra dirección.',
|
|
275
|
+
payment: 'Método de pago',
|
|
276
|
+
choosePayment: 'Elige el método de pago',
|
|
277
|
+
noPayment: 'No hay ningún método de pago disponible para este pedido. Los métodos disponibles dependen del pedido y del país de tu número de teléfono.',
|
|
278
|
+
delivery: 'Envío',
|
|
279
|
+
chooseDelivery: 'Elige el envío',
|
|
280
|
+
selectDelivery: 'Selecciona un método de envío',
|
|
281
|
+
shipment: 'Envío parcial',
|
|
282
|
+
of: 'de',
|
|
283
|
+
shipsFrom: 'Se envía desde',
|
|
284
|
+
digitalDelivery: 'Entrega digital',
|
|
285
|
+
digitalItems: 'Artículos digitales, sin envío',
|
|
286
|
+
splitShipments:
|
|
287
|
+
'Tu pedido se envía en {count} partes desde distintas tiendas. Elige el envío de cada parte.',
|
|
288
|
+
choose: 'Elegir',
|
|
289
|
+
refreshDelivery: 'Actualizar',
|
|
290
|
+
refreshDeliveryHint: 'Actualizar los métodos de envío',
|
|
291
|
+
noDelivery: 'No hay métodos de envío disponibles. Prueba con otra dirección.',
|
|
292
|
+
deliveryChanged:
|
|
293
|
+
'Las condiciones de envío han cambiado. Revisa el método seleccionado y el precio.',
|
|
294
|
+
stockChanged: 'Han cambiado las cantidades o los precios de los artículos. Revisa tu carrito.',
|
|
295
|
+
pickup: 'Elige un punto de recogida',
|
|
296
|
+
pickupAction: 'Elegir',
|
|
297
|
+
noPoints: 'No se ha recibido ningún punto de recogida.',
|
|
298
|
+
pointSearch: 'Busca un punto de recogida por dirección',
|
|
299
|
+
courier: 'Confirmar el envío a la dirección seleccionada',
|
|
300
|
+
courierAction: 'Confirmar',
|
|
301
|
+
pickupType: 'Punto de recogida',
|
|
302
|
+
courierType: 'Envío por mensajero',
|
|
303
|
+
selectedPoint: 'Seleccionado',
|
|
304
|
+
postalCode: 'Código postal',
|
|
305
|
+
pickupMap: 'Puntos de recogida en el mapa',
|
|
306
|
+
mapUnavailable: 'No se ha podido cargar el mapa.',
|
|
307
|
+
estimate: 'Coste estimado del envío',
|
|
308
|
+
goods: 'Artículos',
|
|
309
|
+
discount: 'Descuento',
|
|
310
|
+
promoDiscount: 'Descuento promocional',
|
|
311
|
+
deliveryDiscount: 'Descuento en el envío',
|
|
312
|
+
total: 'Total',
|
|
313
|
+
promo: 'Código promocional',
|
|
314
|
+
apply: 'Aplicar',
|
|
315
|
+
removePromo: 'Quitar',
|
|
316
|
+
removePromoHint: 'Quitar el código promocional',
|
|
317
|
+
promoSignIn: 'Inicia sesión para aplicar un código promocional',
|
|
318
|
+
promoRemoved: 'El código promocional {code} se ha quitado del carrito.',
|
|
319
|
+
promoNotApplied: 'El código promocional {code} no se ha aplicado.',
|
|
320
|
+
promoMinimumOrder: 'Pedido mínimo para este código: {amount}.',
|
|
321
|
+
promoMissingAmount: 'Añade {amount} más de los productos incluidos.',
|
|
322
|
+
// Why a promo code gives no discount, by `promo_notice.reason`. OrionMarketplace's
|
|
323
|
+
// own `message` is shown for a reason this list does not name.
|
|
324
|
+
promoReasons: {
|
|
325
|
+
no_lines: 'Añade artículos al carrito para usar este código promocional.',
|
|
326
|
+
not_matched: 'El código promocional no incluye los artículos de tu carrito.',
|
|
327
|
+
product_discounted:
|
|
328
|
+
'El código promocional no se aplica a artículos que ya tienen descuento.',
|
|
329
|
+
below_minimum: 'El total del pedido no llega al mínimo del código promocional.',
|
|
330
|
+
subscriptions_only: 'Este código promocional es solo para suscripciones.',
|
|
331
|
+
too_small: 'El total del carrito es demasiado bajo para este código promocional.',
|
|
332
|
+
nothing_to_ship:
|
|
333
|
+
'El envío ya es gratuito, así que el código promocional no aporta nada.',
|
|
334
|
+
expired: 'El código promocional ha caducado.',
|
|
335
|
+
not_active: 'El código promocional ya no está activo.',
|
|
336
|
+
not_started: 'La promoción todavía no ha empezado.',
|
|
337
|
+
usage_limit: 'El código promocional se ha usado el número máximo de veces.',
|
|
338
|
+
promotion_limit: 'La promoción se ha agotado.',
|
|
339
|
+
not_yours: 'Este código promocional pertenece a otro cliente.',
|
|
340
|
+
personal_limit:
|
|
341
|
+
'Ya has usado este código promocional el número máximo de veces.',
|
|
342
|
+
first_order_only: 'Este código promocional es solo para la primera compra.',
|
|
343
|
+
wrong_domain: 'Este código promocional no es válido en esta tienda.',
|
|
344
|
+
} as Record<string, string>,
|
|
345
|
+
recipient: 'Destinatario',
|
|
346
|
+
myself: 'Yo',
|
|
347
|
+
myselfHint: 'Yo: recibiré el pedido',
|
|
348
|
+
otherRecipient: 'Otra persona',
|
|
349
|
+
otherRecipientHint: 'Otro destinatario',
|
|
350
|
+
name: 'Nombre completo',
|
|
351
|
+
phone: 'Número de teléfono',
|
|
352
|
+
comment: 'Comentario del pedido',
|
|
353
|
+
options: 'Opciones del artículo',
|
|
354
|
+
optionLimit: 'Cantidad máxima: {count}',
|
|
355
|
+
optionUnavailable: 'Esta opción no se puede añadir ahora.',
|
|
356
|
+
optionSelected: 'También seleccionado',
|
|
357
|
+
optionSingle: 'Una opción',
|
|
358
|
+
optionRequired: 'Obligatorio',
|
|
359
|
+
optionMissing: 'Necesario para hacer el pedido',
|
|
360
|
+
optionMultiple: 'Varias opciones',
|
|
361
|
+
optionFile: 'Adjuntar un archivo',
|
|
362
|
+
optionText: 'Tu texto',
|
|
363
|
+
selectedFile: 'Archivo adjunto',
|
|
364
|
+
optionFileLimit: 'Hasta {size} MB',
|
|
365
|
+
optionFileTooLarge: 'El archivo supera los {size} MB.',
|
|
366
|
+
clearOption: 'Borrar',
|
|
367
|
+
clearOptionHint: 'Borrar el texto y el archivo guardados',
|
|
368
|
+
placeOrder: 'Pedido',
|
|
369
|
+
created: 'Pedido creado',
|
|
370
|
+
viewOrder: 'Ver el pedido',
|
|
371
|
+
paymentLinkFailed:
|
|
372
|
+
'El pedido está hecho, pero no se ha podido crear el enlace de pago: {error}. Paga el pedido más tarde desde su página.',
|
|
373
|
+
payLater: 'Abrir el pedido para pagar',
|
|
374
|
+
review: 'Revisa los artículos, el envío y el total antes de hacer el pedido.',
|
|
375
|
+
refreshRequired: 'Actualiza tu carrito antes de continuar.',
|
|
376
|
+
orderUncertain:
|
|
377
|
+
'No se ha podido confirmar el pedido. Revisa tu historial de pedidos antes de volver a intentarlo.',
|
|
378
|
+
};
|
|
379
|
+
|
|
253
380
|
/**
|
|
254
381
|
* The notice of an order placed without a payment link: the backend's reason
|
|
255
382
|
* is shown as it came, without its closing full stop.
|
|
@@ -259,5 +386,5 @@ export function cartPaymentLinkError(template: string, error: string) {
|
|
|
259
386
|
}
|
|
260
387
|
|
|
261
388
|
export function cartLabels(locale: string) {
|
|
262
|
-
return locale.toLowerCase().startsWith('ru') ? ru : en;
|
|
389
|
+
return locale.toLowerCase().startsWith('ru') ? ru : locale.toLowerCase().startsWith('es') ? es : en;
|
|
263
390
|
}
|
|
@@ -92,6 +92,21 @@ export function cartPreviewLabels(locale: string) {
|
|
|
92
92
|
checkout: 'Перейти в корзину',
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
+
if (locale.toLowerCase().startsWith('es')) {
|
|
96
|
+
return {
|
|
97
|
+
title: 'Carrito',
|
|
98
|
+
quantity: 'Artículos',
|
|
99
|
+
pieces: (count: number) => `Cant. ${count}`,
|
|
100
|
+
empty: 'Tu carrito está vacío',
|
|
101
|
+
loading: 'Cargando el carrito…',
|
|
102
|
+
failed: 'No se ha podido cargar el carrito. Inténtalo de nuevo.',
|
|
103
|
+
unavailable: 'La vista previa del carrito no está disponible.',
|
|
104
|
+
retry: 'Reintentar',
|
|
105
|
+
remove: 'Quitar del carrito',
|
|
106
|
+
removeFailed: 'No se ha podido quitar el artículo. Inténtalo de nuevo.',
|
|
107
|
+
checkout: 'Ir al carrito',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
95
110
|
return {
|
|
96
111
|
title: 'Cart',
|
|
97
112
|
quantity: 'Items',
|
|
@@ -14,7 +14,20 @@ export function categoryLabels(locale: string) {
|
|
|
14
14
|
page: 'Страница',
|
|
15
15
|
of: 'из',
|
|
16
16
|
}
|
|
17
|
-
: {
|
|
17
|
+
: locale.toLowerCase().startsWith('es') ? {
|
|
18
|
+
breadcrumb: 'Ruta de la categoría',
|
|
19
|
+
catalog: 'Catálogo',
|
|
20
|
+
category: 'Categoría',
|
|
21
|
+
subcategories: 'Explorar esta categoría',
|
|
22
|
+
products: 'Productos',
|
|
23
|
+
results: 'Productos encontrados',
|
|
24
|
+
empty: 'Ningún producto de esta categoría coincide con los filtros seleccionados.',
|
|
25
|
+
clear: 'Borrar los filtros',
|
|
26
|
+
missing: 'Categoría no encontrada',
|
|
27
|
+
missingHint: 'Elige una categoría disponible del catálogo.',
|
|
28
|
+
page: 'Página',
|
|
29
|
+
of: 'de',
|
|
30
|
+
} : {
|
|
18
31
|
breadcrumb: 'Category path',
|
|
19
32
|
catalog: 'Catalog',
|
|
20
33
|
category: 'Category',
|
|
@@ -97,6 +97,56 @@ const ru: typeof en = {
|
|
|
97
97
|
'Подтвердите электронную почту, чтобы получить промокод. При записи на акцию письма включаются.',
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
const es: typeof en = {
|
|
101
|
+
pageTitle: 'Información de contacto',
|
|
102
|
+
intro: 'Consulta los datos de la empresa o envía una pregunta a la tienda.',
|
|
103
|
+
company: 'Datos de la empresa',
|
|
104
|
+
companyName: 'Empresa',
|
|
105
|
+
legalName: 'Razón social',
|
|
106
|
+
address: 'Dirección',
|
|
107
|
+
phone: 'Teléfono',
|
|
108
|
+
social: 'Redes sociales',
|
|
109
|
+
registration: 'Datos de registro',
|
|
110
|
+
taxId: 'NIF',
|
|
111
|
+
registrationNumber: 'Número de registro',
|
|
112
|
+
copy: 'Copiar',
|
|
113
|
+
copied: 'Copiado',
|
|
114
|
+
copyAddress: 'Copiar la dirección',
|
|
115
|
+
copyPhone: 'Copiar el número de teléfono',
|
|
116
|
+
copyTaxId: 'Copiar el NIF',
|
|
117
|
+
copyRegistrationNumber: 'Copiar el número de registro',
|
|
118
|
+
copiedStatus: 'Copiado al portapapeles.',
|
|
119
|
+
copyFailed: 'No se ha podido copiar. Selecciona el texto.',
|
|
120
|
+
title: 'Contáctanos',
|
|
121
|
+
description: 'Rellena el formulario y la tienda recibirá tu pregunta.',
|
|
122
|
+
name: 'Nombre',
|
|
123
|
+
email: 'Correo electrónico',
|
|
124
|
+
message: 'Mensaje',
|
|
125
|
+
messageHint: 'De 15 a 2048 caracteres.',
|
|
126
|
+
captcha: 'Código de la imagen',
|
|
127
|
+
refreshCaptcha: 'Actualizar',
|
|
128
|
+
refreshCaptchaHint: 'Cambiar la imagen',
|
|
129
|
+
send: 'Enviar',
|
|
130
|
+
sent: 'Tu mensaje se ha enviado a la tienda.',
|
|
131
|
+
another: 'Otro',
|
|
132
|
+
anotherHint: 'Escribir otro mensaje',
|
|
133
|
+
unsubscribe: 'Preferencias de correo',
|
|
134
|
+
unsubscribeAction: 'Cancelar aviso',
|
|
135
|
+
newsletter: 'Dejar de recibir boletines',
|
|
136
|
+
products: 'Dejar de recibir correos de disponibilidad',
|
|
137
|
+
unsubscribeDone: 'Te has dado de baja de estos correos.',
|
|
138
|
+
invalidLink:
|
|
139
|
+
'Este enlace está incompleto. Abre el enlace de baja desde tu correo.',
|
|
140
|
+
event: 'Participa en esta oferta',
|
|
141
|
+
eventDescription: 'Confirma tu participación para recibir la oferta por correo.',
|
|
142
|
+
eventJoin: 'Participar',
|
|
143
|
+
eventDoneTitle: 'Ya participas',
|
|
144
|
+
eventDone: 'El código promocional se enviará a tu correo confirmado.',
|
|
145
|
+
eventDoneAddress: 'Correo de entrega',
|
|
146
|
+
eventEmail:
|
|
147
|
+
'Confirma una dirección de correo para recibir el código promocional. Al participar se activan los correos.',
|
|
148
|
+
};
|
|
149
|
+
|
|
100
150
|
export function contactLabels(locale: string) {
|
|
101
|
-
return locale.startsWith('ru') ? ru : en;
|
|
151
|
+
return locale.startsWith('ru') ? ru : locale.startsWith('es') ? es : en;
|
|
102
152
|
}
|