wineater-bot 4.9.8 → 4.12.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.
- package/dist/wineater-chatbot.mjs +3586 -3469
- package/dist/wineater-chatbot.umd.js +29 -24
- package/package.json +1 -1
- package/src/App.svelte +94 -1
- package/src/components/WineTile.svelte +60 -0
- package/src/services/analytics/constants.ts +2 -0
- package/src/services/api/api.ts +50 -2
- package/src/services/api/deserialize.ts +18 -0
- package/src/services/i18n/translations.json +27 -0
package/package.json
CHANGED
package/src/App.svelte
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { setCookie, setUniqueUserCookie, trackWidgetEvent } from './services/analytics/analytics';
|
|
3
3
|
import Tracker from '@openreplay/tracker';
|
|
4
4
|
import type { RecommendedWine, Wine, WineId } from './services/api/api';
|
|
5
|
-
import { wineaterApi } from './services/api/api';
|
|
5
|
+
import { wineaterApi, apiVersion } from './services/api/api';
|
|
6
6
|
import { onMount } from 'svelte';
|
|
7
7
|
import { WIDGET_EVENTS } from './services/analytics/constants';
|
|
8
8
|
import Header from './components/Header.svelte';
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import Search from './components/Search.svelte';
|
|
11
11
|
import Loader from './components/Loader.svelte';
|
|
12
12
|
import WineTile from './components/WineTile.svelte';
|
|
13
|
+
import Action from './components/ui/Action.svelte';
|
|
13
14
|
import { appStore } from './services/store/app-store.svelte';
|
|
14
15
|
import { getTranslation, translateTexts } from './services/i18n/i18n';
|
|
15
16
|
import { generateInlineStyle } from './services/ui-config/utils';
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
|
|
31
32
|
let initialized = $state(false);
|
|
32
33
|
let isLoading = $state(false);
|
|
34
|
+
let isLoadingMore = $state(false);
|
|
33
35
|
let results = $state<RecommendedWine[] | Wine[]>([]);
|
|
34
36
|
let selectedWineId = $state<WineId>(null);
|
|
35
37
|
let searchQuery = $state('');
|
|
@@ -37,6 +39,14 @@
|
|
|
37
39
|
let isAnimating = $state(false);
|
|
38
40
|
let hoveredWineId = $state(null);
|
|
39
41
|
let translatedPopups = $state([]);
|
|
42
|
+
// Последний реально выполненный поиск (с добавленным ';') — searchQuery
|
|
43
|
+
// может измениться в поле ввода до следующего сабмита, а "ещё вариантов"
|
|
44
|
+
// должен повторять именно тот запрос, что дал текущие results.
|
|
45
|
+
let lastSearchedQuery = $state('');
|
|
46
|
+
let hasMore = $state(false);
|
|
47
|
+
// Всё, что уже показывали по текущему запросу — «Try again» заменяет выдачу,
|
|
48
|
+
// и без накопления повторный клик вернул бы те же вина.
|
|
49
|
+
let seenIds = $state<string[]>([]);
|
|
40
50
|
let appLanguage = $derived(appStore.getAppLanguage());
|
|
41
51
|
let storeLanguage = $derived(appStore.getStoreLanguage());
|
|
42
52
|
let apiKey = $derived(appStore.getApiKey());
|
|
@@ -130,8 +140,11 @@
|
|
|
130
140
|
// шаг 2: очистить список и сбросить флаг
|
|
131
141
|
results = [];
|
|
132
142
|
isAnimating = false;
|
|
143
|
+
hasMore = false;
|
|
144
|
+
seenIds = [];
|
|
133
145
|
|
|
134
146
|
query = searchQuery.trim() + ';';
|
|
147
|
+
lastSearchedQuery = query;
|
|
135
148
|
try {
|
|
136
149
|
trackWidgetEvent(WIDGET_EVENTS.SEARCH_PERFORMED, storeId, { query });
|
|
137
150
|
} catch (error) {
|
|
@@ -148,6 +161,10 @@
|
|
|
148
161
|
trackWidgetEvent(WIDGET_EVENTS.NO_RESULTS_FOUND, storeId, { query });
|
|
149
162
|
} else {
|
|
150
163
|
results = searchResults;
|
|
164
|
+
// Пагинация доступна только для search/v2 — у instant/legacy-путей нет
|
|
165
|
+
// "more"-эндпоинта. Неполная выдача (< 4) означает, что кандидаты и так
|
|
166
|
+
// кончились: кнопка в этом случае только мигнула бы впустую.
|
|
167
|
+
hasMore = apiVersion === '2' && searchResults.length >= 4;
|
|
151
168
|
trackWidgetEvent(WIDGET_EVENTS.SEARCH_RESULTS_DISPLAYED, storeId, {
|
|
152
169
|
results_count: searchResults.length,
|
|
153
170
|
results: searchResults,
|
|
@@ -167,6 +184,68 @@
|
|
|
167
184
|
}
|
|
168
185
|
}
|
|
169
186
|
|
|
187
|
+
async function handleShowMore() {
|
|
188
|
+
if (isLoadingMore || !hasMore) return;
|
|
189
|
+
try {
|
|
190
|
+
isLoadingMore = true;
|
|
191
|
+
// Карточки заменяются, а не дописываются, поэтому исключать надо всё
|
|
192
|
+
// показанное за сессию — иначе следующий клик вернёт те же вина.
|
|
193
|
+
seenIds = [...new Set([...seenIds, ...results.map((w) => w.id)])];
|
|
194
|
+
const { recommendations, hasMore: more } = await wineaterApi.getMoreWineRecommendations(
|
|
195
|
+
lastSearchedQuery,
|
|
196
|
+
seenIds
|
|
197
|
+
);
|
|
198
|
+
trackWidgetEvent(WIDGET_EVENTS.SHOW_MORE_CLICKED, storeId, {
|
|
199
|
+
query: lastSearchedQuery,
|
|
200
|
+
results_count: recommendations.length
|
|
201
|
+
});
|
|
202
|
+
if (recommendations.length > 0) {
|
|
203
|
+
results = recommendations;
|
|
204
|
+
}
|
|
205
|
+
hasMore = more;
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('[wineater] show more error:', error);
|
|
208
|
+
hasMore = false;
|
|
209
|
+
} finally {
|
|
210
|
+
isLoadingMore = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function handleSimilarClick(wine: Wine | RecommendedWine) {
|
|
215
|
+
try {
|
|
216
|
+
isLoading = true;
|
|
217
|
+
clearError();
|
|
218
|
+
trackWidgetEvent(WIDGET_EVENTS.SIMILAR_WINES_CLICKED, storeId, {
|
|
219
|
+
wine_id: wine.id,
|
|
220
|
+
wine_title: wine.title
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await animateClearAll(220);
|
|
224
|
+
results = [];
|
|
225
|
+
isAnimating = false;
|
|
226
|
+
hasMore = false;
|
|
227
|
+
selectedWineId = null;
|
|
228
|
+
|
|
229
|
+
const similarWines = await wineaterApi.getSimilarWines(wine.id);
|
|
230
|
+
searchQuery = wine.title;
|
|
231
|
+
lastSearchedQuery = '';
|
|
232
|
+
|
|
233
|
+
if (!similarWines || similarWines.length === 0) {
|
|
234
|
+
results = [];
|
|
235
|
+
translatedPopups = [];
|
|
236
|
+
errorMessage = getTranslation({ key: 'noResultsMessage', lang: appLanguage });
|
|
237
|
+
} else {
|
|
238
|
+
results = similarWines;
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
results = [];
|
|
242
|
+
console.error('[wineater] similar wines error:', error);
|
|
243
|
+
setSearchError();
|
|
244
|
+
} finally {
|
|
245
|
+
isLoading = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
170
249
|
$effect(async () => {
|
|
171
250
|
// whenever results change, recompute translatedPopups asynchronously
|
|
172
251
|
if (!results || results.length === 0) {
|
|
@@ -325,12 +404,20 @@
|
|
|
325
404
|
onBuyClick={handleBuyBtnClick}
|
|
326
405
|
onBuyLinkClick={handleBuyLinkClick}
|
|
327
406
|
onPopupClose={closePopup}
|
|
407
|
+
onSimilarClick={handleSimilarClick}
|
|
328
408
|
{onTileEnter}
|
|
329
409
|
{onTileLeave}
|
|
330
410
|
{onTileClick}
|
|
331
411
|
/>
|
|
332
412
|
{/each}
|
|
333
413
|
</div>
|
|
414
|
+
{#if hasMore}
|
|
415
|
+
<div class="wr-App-showMore">
|
|
416
|
+
<Action handleClick={handleShowMore} variant="outlined">
|
|
417
|
+
{getTranslation({ key: 'tryAgain', lang: appLanguage })}
|
|
418
|
+
</Action>
|
|
419
|
+
</div>
|
|
420
|
+
{/if}
|
|
334
421
|
{/if}
|
|
335
422
|
</div>
|
|
336
423
|
|
|
@@ -402,6 +489,12 @@
|
|
|
402
489
|
margin-bottom: 0;
|
|
403
490
|
}
|
|
404
491
|
|
|
492
|
+
.wr-App-showMore {
|
|
493
|
+
display: flex;
|
|
494
|
+
justify-content: center;
|
|
495
|
+
padding: 16px 0 24px;
|
|
496
|
+
}
|
|
497
|
+
|
|
405
498
|
/* ===================== 12) Error Message ===================== */
|
|
406
499
|
.wr-App-error {
|
|
407
500
|
text-align: center;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
onBuyClick,
|
|
20
20
|
onBuyLinkClick,
|
|
21
21
|
onPopupClose,
|
|
22
|
+
onSimilarClick,
|
|
22
23
|
isAnimating,
|
|
23
24
|
translatedPopup
|
|
24
25
|
}: {
|
|
@@ -32,6 +33,7 @@
|
|
|
32
33
|
onBuyClick: (wine: Wine | RecommendedWine, index: number) => void;
|
|
33
34
|
onBuyLinkClick: (wine: Wine | RecommendedWine, index: number) => void;
|
|
34
35
|
onPopupClose: () => void;
|
|
36
|
+
onSimilarClick?: (wine: Wine | RecommendedWine) => void;
|
|
35
37
|
isAnimating: boolean;
|
|
36
38
|
translatedPopup: Partial<RecommendedWine> | null;
|
|
37
39
|
} = $props();
|
|
@@ -101,6 +103,12 @@
|
|
|
101
103
|
</div>
|
|
102
104
|
{/if}
|
|
103
105
|
|
|
106
|
+
{#if wine.byTheGlass}
|
|
107
|
+
<div class="wr-WineTile-glass">
|
|
108
|
+
{getTranslation({ key: 'byTheGlass', lang })}{wine.glassPrice ? ` — ${formatPrice(wine.glassPrice)}` : ''}
|
|
109
|
+
</div>
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
104
112
|
<h3 class="wr-WineTile-title">{wine.producer ? wine.producer : ''} {wine.title}</h3>
|
|
105
113
|
</header>
|
|
106
114
|
|
|
@@ -132,6 +140,15 @@
|
|
|
132
140
|
{getTranslation({ key: 'moreInfo', lang })}
|
|
133
141
|
</Action>
|
|
134
142
|
{/if}
|
|
143
|
+
{#if onSimilarClick}
|
|
144
|
+
<Action
|
|
145
|
+
handleClick={() => onSimilarClick(wine)}
|
|
146
|
+
variant="text"
|
|
147
|
+
className="wr-WineTile-action wr-WineTile-action--similar js-WineTileAction"
|
|
148
|
+
>
|
|
149
|
+
{getTranslation({ key: 'similarWines', lang })}
|
|
150
|
+
</Action>
|
|
151
|
+
{/if}
|
|
135
152
|
</div>
|
|
136
153
|
</footer>
|
|
137
154
|
|
|
@@ -255,8 +272,51 @@
|
|
|
255
272
|
color: #333;
|
|
256
273
|
}
|
|
257
274
|
|
|
275
|
+
.wr-WineTile-glass {
|
|
276
|
+
font-size: 12px;
|
|
277
|
+
font-weight: 500;
|
|
278
|
+
color: var(--wr-brand-color, #7b2beb);
|
|
279
|
+
margin-bottom: 8px;
|
|
280
|
+
}
|
|
281
|
+
|
|
258
282
|
.wr-WineTile-actions {
|
|
259
283
|
margin: 16px auto 0;
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* :global обязателен — класс уезжает внутрь <Action>, а скоуп Svelte
|
|
291
|
+
действует только на разметку этого компонента, иначе правило отбрасывается
|
|
292
|
+
как неиспользуемое. Цвет тот же, что у Buy; подчёркивание выезжает слева. */
|
|
293
|
+
:global(.wr-WineTile-action--similar) {
|
|
294
|
+
font-size: 14px;
|
|
295
|
+
font-weight: 500;
|
|
296
|
+
color: var(--wr-brand-color, #7b2beb);
|
|
297
|
+
text-decoration: none;
|
|
298
|
+
background: transparent;
|
|
299
|
+
border: none;
|
|
300
|
+
padding: 0;
|
|
301
|
+
position: relative;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
:global(.wr-WineTile-action--similar)::after {
|
|
305
|
+
content: '';
|
|
306
|
+
position: absolute;
|
|
307
|
+
left: 0;
|
|
308
|
+
bottom: -2px;
|
|
309
|
+
width: 100%;
|
|
310
|
+
height: 1px;
|
|
311
|
+
background-color: currentColor;
|
|
312
|
+
transform: scaleX(0);
|
|
313
|
+
transform-origin: left;
|
|
314
|
+
transition: transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
:global(.wr-WineTile-action--similar:hover)::after,
|
|
318
|
+
:global(.wr-WineTile-action--similar:focus-visible)::after {
|
|
319
|
+
transform: scaleX(1);
|
|
260
320
|
}
|
|
261
321
|
|
|
262
322
|
/* Hover popup inside tile */
|
|
@@ -9,6 +9,8 @@ export const WIDGET_EVENTS = {
|
|
|
9
9
|
WINE_DETAILS_VIEWED: 'wine_details_viewed',
|
|
10
10
|
WINE_DETAILS_CLOSED: 'wine_details_closed',
|
|
11
11
|
BUY_BUTTON_CLICKED: 'buy_button_clicked',
|
|
12
|
+
SHOW_MORE_CLICKED: 'show_more_clicked',
|
|
13
|
+
SIMILAR_WINES_CLICKED: 'similar_wines_clicked',
|
|
12
14
|
FILTER_APPLIED: 'filter_applied',
|
|
13
15
|
LANGUAGE_CHANGED: 'language_changed',
|
|
14
16
|
ERROR_OCCURRED: 'error_occurred',
|
package/src/services/api/api.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deserializeInstantRecommendation, deserializeRecommendedWine } from "./deserialize";
|
|
1
|
+
import { deserializeInstantRecommendation, deserializeRecommendedWine, deserializeMoreRecommendations } from "./deserialize";
|
|
2
2
|
|
|
3
3
|
export type WineId = string;
|
|
4
4
|
export type WinePrice = number;
|
|
@@ -25,6 +25,8 @@ export type Wine = {
|
|
|
25
25
|
itemId: string | null;
|
|
26
26
|
isPromote: boolean | null;
|
|
27
27
|
stock: number | null;
|
|
28
|
+
byTheGlass: boolean;
|
|
29
|
+
glassPrice: number | null;
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export type RecommendedWine = Omit<Wine, "shortDescription" | "producer" | "pairingRecommendation" | "itemId"> & {
|
|
@@ -96,6 +98,52 @@ class WineaterApi {
|
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
async getMoreWineRecommendations(text: string, excludeIds: string[]): Promise<{ recommendations: RecommendedWine[]; hasMore: boolean }> {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(`${this.#baseUrl}dish/recommendations/search/v2/more`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'Authorization': `Bearer ${this.#token}`
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({ text, exclude_ids: excludeIds })
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return deserializeMoreRecommendations(data);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new Error(`Error in getMoreWineRecommendations: ${error.message}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getSimilarWines(id: WineId): Promise<RecommendedWine[]> {
|
|
125
|
+
try {
|
|
126
|
+
const response = await fetch(`${this.#baseUrl}dish/recommendations/similar`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
'Authorization': `Bearer ${this.#token}`
|
|
131
|
+
},
|
|
132
|
+
body: JSON.stringify({ id })
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
throw new Error(`API call failed with status ${response.status}: ${data.message || 'No additional error information'}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return data.filter(Boolean).map(deserializeRecommendedWine);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`Error in getSimilarWines: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
99
147
|
// TODO: define store type
|
|
100
148
|
async getStoreConfig(): Promise<any> {
|
|
101
149
|
try {
|
|
@@ -117,5 +165,5 @@ class WineaterApi {
|
|
|
117
165
|
|
|
118
166
|
const DEMO_TOKEN = 'HeUw6IXuhvz6tRuXdPEGEbj3SGQ3Q2YtEc1Z8jd41Kg9LtyPb4Epu0to31SYIadG';
|
|
119
167
|
const token = window?.wineaterData?.CLIENT_TOKEN || DEMO_TOKEN;
|
|
120
|
-
const apiVersion = String(window?.wineaterData?.apiVersion || '1');
|
|
168
|
+
export const apiVersion = String(window?.wineaterData?.apiVersion || '1');
|
|
121
169
|
export const wineaterApi = new WineaterApi(token, apiVersion)
|
|
@@ -23,6 +23,8 @@ type RawInstantRecommendation = {
|
|
|
23
23
|
item_id?: string;
|
|
24
24
|
is_promote?: boolean;
|
|
25
25
|
stock?: string;
|
|
26
|
+
by_the_glass?: boolean;
|
|
27
|
+
glass_price?: string | number;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
export function deserializeInstantRecommendation(raw: RawInstantRecommendation): Wine {
|
|
@@ -49,6 +51,8 @@ export function deserializeInstantRecommendation(raw: RawInstantRecommendation):
|
|
|
49
51
|
itemId: raw.item_id || null,
|
|
50
52
|
isPromote: raw.is_promote ? Boolean(raw.is_promote) : null,
|
|
51
53
|
stock: raw.stock ? Number(raw.stock) : null,
|
|
54
|
+
byTheGlass: Boolean(raw.by_the_glass),
|
|
55
|
+
glassPrice: raw.glass_price != null ? Number(raw.glass_price) : null,
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -74,6 +78,8 @@ type RawRecommendedWine = {
|
|
|
74
78
|
distance: string;
|
|
75
79
|
reason: string;
|
|
76
80
|
type: string;
|
|
81
|
+
by_the_glass?: boolean;
|
|
82
|
+
glass_price?: string | number;
|
|
77
83
|
}
|
|
78
84
|
export function deserializeRecommendedWine(raw: RawRecommendedWine): RecommendedWine {
|
|
79
85
|
return {
|
|
@@ -98,5 +104,17 @@ export function deserializeRecommendedWine(raw: RawRecommendedWine): Recommended
|
|
|
98
104
|
distance: Number(raw.distance),
|
|
99
105
|
reason: String(raw.reason),
|
|
100
106
|
type: String(raw.type),
|
|
107
|
+
byTheGlass: Boolean(raw.by_the_glass),
|
|
108
|
+
glassPrice: raw.glass_price != null ? Number(raw.glass_price) : null,
|
|
101
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function deserializeMoreRecommendations(raw: {
|
|
113
|
+
has_more: boolean;
|
|
114
|
+
recommendations: RawRecommendedWine[];
|
|
115
|
+
}): { recommendations: RecommendedWine[]; hasMore: boolean } {
|
|
116
|
+
return {
|
|
117
|
+
recommendations: (raw.recommendations || []).filter(Boolean).map(deserializeRecommendedWine),
|
|
118
|
+
hasMore: Boolean(raw.has_more),
|
|
119
|
+
};
|
|
102
120
|
}
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"type": "Type",
|
|
18
18
|
"reason": "Suggestion",
|
|
19
19
|
"moreInfo": "Plus d'infos",
|
|
20
|
+
"tryAgain": "Réessayer",
|
|
21
|
+
"similarWines": "Vins similaires",
|
|
22
|
+
"byTheGlass": "Au verre",
|
|
20
23
|
"winePairingIntro": "Pour ce vin, voici quelques suggestions de plats :",
|
|
21
24
|
"askForDinner": "Que voulez-vous manger ce soir?",
|
|
22
25
|
"clearChatButton": "Effacer le chat",
|
|
@@ -80,6 +83,9 @@
|
|
|
80
83
|
"type": "Tipo",
|
|
81
84
|
"reason": "Sugerencia",
|
|
82
85
|
"moreInfo": "Más info",
|
|
86
|
+
"tryAgain": "Probar otra vez",
|
|
87
|
+
"similarWines": "Vinos similares",
|
|
88
|
+
"byTheGlass": "Por copa",
|
|
83
89
|
"winePairingIntro": "Para este vino, aquí tienes algunas sugerencias de platos:",
|
|
84
90
|
"askForDinner": "¿Qué te gustaría cenar?",
|
|
85
91
|
"clearChatButton": "Limpiar el chat",
|
|
@@ -143,6 +149,9 @@
|
|
|
143
149
|
"type": "Type",
|
|
144
150
|
"reason": "Suggestion",
|
|
145
151
|
"moreInfo": "More info",
|
|
152
|
+
"tryAgain": "Try again",
|
|
153
|
+
"similarWines": "Similar wines",
|
|
154
|
+
"byTheGlass": "By the glass",
|
|
146
155
|
"winePairingIntro": "For this wine, here are some suggested dishes:",
|
|
147
156
|
"askForDinner": "What would you like to have for dinner?",
|
|
148
157
|
"clearChatButton": "Clear Chat",
|
|
@@ -206,6 +215,9 @@
|
|
|
206
215
|
"type": "Tip",
|
|
207
216
|
"reason": "Preporuka",
|
|
208
217
|
"moreInfo": "Više informacija",
|
|
218
|
+
"tryAgain": "Pokušaj ponovo",
|
|
219
|
+
"similarWines": "Slična vina",
|
|
220
|
+
"byTheGlass": "Na čašu",
|
|
209
221
|
"winePairingIntro": "Za ovo vino, preporučuju se sledeća jela:",
|
|
210
222
|
"askForDinner": "Šta biste želeli za večeru?",
|
|
211
223
|
"clearChatButton": "Obriši razgovor",
|
|
@@ -269,6 +281,9 @@
|
|
|
269
281
|
"type": "Typ",
|
|
270
282
|
"reason": "Anregung",
|
|
271
283
|
"moreInfo": "Mehr Info",
|
|
284
|
+
"tryAgain": "Nochmal versuchen",
|
|
285
|
+
"similarWines": "Ähnliche Weine",
|
|
286
|
+
"byTheGlass": "Glasweise",
|
|
272
287
|
"winePairingIntro": "Für diesen Wein gibt es folgende Essensvorschläge:",
|
|
273
288
|
"askForDinner": "Was möchten Sie zum Abendessen essen?",
|
|
274
289
|
"clearChatButton": "Chat löschen",
|
|
@@ -331,6 +346,9 @@
|
|
|
331
346
|
"type": "Тип",
|
|
332
347
|
"reason": "Рекомендація",
|
|
333
348
|
"moreInfo": "Докладніше",
|
|
349
|
+
"tryAgain": "Спробувати ще",
|
|
350
|
+
"similarWines": "Схожі вина",
|
|
351
|
+
"byTheGlass": "На келих",
|
|
334
352
|
"winePairingIntro": "До цього вина підходять такі страви:",
|
|
335
353
|
"askForDinner": "Що ви хочете на вечерю?",
|
|
336
354
|
"clearChatButton": "Очистити чат",
|
|
@@ -394,6 +412,9 @@
|
|
|
394
412
|
"type": "Тип",
|
|
395
413
|
"reason": "Рекомендация",
|
|
396
414
|
"moreInfo": "Подробнее",
|
|
415
|
+
"tryAgain": "Попробовать ещё",
|
|
416
|
+
"similarWines": "Похожие вина",
|
|
417
|
+
"byTheGlass": "По бокалам",
|
|
397
418
|
"winePairingIntro": "К этому вину подходят следующие блюда:",
|
|
398
419
|
"askForDinner": "Что бы вы хотели на ужин?",
|
|
399
420
|
"clearChatButton": "Очистить чат",
|
|
@@ -457,6 +478,9 @@
|
|
|
457
478
|
"type": "Түрі",
|
|
458
479
|
"reason": "Ұсыныс",
|
|
459
480
|
"moreInfo": "Толығырақ",
|
|
481
|
+
"tryAgain": "Қайта көру",
|
|
482
|
+
"similarWines": "Ұқсас шараптар",
|
|
483
|
+
"byTheGlass": "Бокалмен",
|
|
460
484
|
"winePairingIntro": "Бұл шарапқа келесі тағамдар сәйкес келеді:",
|
|
461
485
|
"askForDinner": "Кешкі асқа не қалайсыз?",
|
|
462
486
|
"clearChatButton": "Чатты тазалау",
|
|
@@ -520,6 +544,9 @@
|
|
|
520
544
|
"type": "Tipo",
|
|
521
545
|
"reason": "Sugestão",
|
|
522
546
|
"moreInfo": "Mais informações",
|
|
547
|
+
"tryAgain": "Tentar de novo",
|
|
548
|
+
"similarWines": "Vinhos semelhantes",
|
|
549
|
+
"byTheGlass": "Por taça",
|
|
523
550
|
"winePairingIntro": "Para este vinho, aqui estão algumas sugestões de pratos:",
|
|
524
551
|
"askForDinner": "O que você gostaria de jantar?",
|
|
525
552
|
"clearChatButton": "Limpar chat",
|