wineater-bot 4.9.7 → 4.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.
- package/dist/wineater-chatbot.mjs +3604 -3485
- package/dist/wineater-chatbot.umd.js +24 -24
- package/package.json +1 -1
- package/src/App.svelte +96 -1
- package/src/components/WineTile.svelte +38 -0
- package/src/components/ui/Action.svelte +1 -1
- 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,11 @@
|
|
|
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);
|
|
40
47
|
let appLanguage = $derived(appStore.getAppLanguage());
|
|
41
48
|
let storeLanguage = $derived(appStore.getStoreLanguage());
|
|
42
49
|
let apiKey = $derived(appStore.getApiKey());
|
|
@@ -130,8 +137,10 @@
|
|
|
130
137
|
// шаг 2: очистить список и сбросить флаг
|
|
131
138
|
results = [];
|
|
132
139
|
isAnimating = false;
|
|
140
|
+
hasMore = false;
|
|
133
141
|
|
|
134
142
|
query = searchQuery.trim() + ';';
|
|
143
|
+
lastSearchedQuery = query;
|
|
135
144
|
try {
|
|
136
145
|
trackWidgetEvent(WIDGET_EVENTS.SEARCH_PERFORMED, storeId, { query });
|
|
137
146
|
} catch (error) {
|
|
@@ -148,6 +157,10 @@
|
|
|
148
157
|
trackWidgetEvent(WIDGET_EVENTS.NO_RESULTS_FOUND, storeId, { query });
|
|
149
158
|
} else {
|
|
150
159
|
results = searchResults;
|
|
160
|
+
// Пагинация доступна только для search/v2 — у instant/legacy-путей нет
|
|
161
|
+
// "more"-эндпоинта. Неполная выдача (< 4) означает, что кандидаты и так
|
|
162
|
+
// кончились: кнопка в этом случае только мигнула бы впустую.
|
|
163
|
+
hasMore = apiVersion === '2' && searchResults.length >= 4;
|
|
151
164
|
trackWidgetEvent(WIDGET_EVENTS.SEARCH_RESULTS_DISPLAYED, storeId, {
|
|
152
165
|
results_count: searchResults.length,
|
|
153
166
|
results: searchResults,
|
|
@@ -167,6 +180,64 @@
|
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
182
|
|
|
183
|
+
async function handleShowMore() {
|
|
184
|
+
if (isLoadingMore || !hasMore) return;
|
|
185
|
+
try {
|
|
186
|
+
isLoadingMore = true;
|
|
187
|
+
const excludeIds = results.map((w) => w.id);
|
|
188
|
+
const { recommendations, hasMore: more } = await wineaterApi.getMoreWineRecommendations(
|
|
189
|
+
lastSearchedQuery,
|
|
190
|
+
excludeIds
|
|
191
|
+
);
|
|
192
|
+
trackWidgetEvent(WIDGET_EVENTS.SHOW_MORE_CLICKED, storeId, {
|
|
193
|
+
query: lastSearchedQuery,
|
|
194
|
+
results_count: recommendations.length
|
|
195
|
+
});
|
|
196
|
+
results = [...results, ...recommendations];
|
|
197
|
+
hasMore = more;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[wineater] show more error:', error);
|
|
200
|
+
hasMore = false;
|
|
201
|
+
} finally {
|
|
202
|
+
isLoadingMore = false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function handleSimilarClick(wine: Wine | RecommendedWine) {
|
|
207
|
+
try {
|
|
208
|
+
isLoading = true;
|
|
209
|
+
clearError();
|
|
210
|
+
trackWidgetEvent(WIDGET_EVENTS.SIMILAR_WINES_CLICKED, storeId, {
|
|
211
|
+
wine_id: wine.id,
|
|
212
|
+
wine_title: wine.title
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await animateClearAll(220);
|
|
216
|
+
results = [];
|
|
217
|
+
isAnimating = false;
|
|
218
|
+
hasMore = false;
|
|
219
|
+
selectedWineId = null;
|
|
220
|
+
|
|
221
|
+
const similarWines = await wineaterApi.getSimilarWines(wine.id);
|
|
222
|
+
searchQuery = wine.title;
|
|
223
|
+
lastSearchedQuery = '';
|
|
224
|
+
|
|
225
|
+
if (!similarWines || similarWines.length === 0) {
|
|
226
|
+
results = [];
|
|
227
|
+
translatedPopups = [];
|
|
228
|
+
errorMessage = getTranslation({ key: 'noResultsMessage', lang: appLanguage });
|
|
229
|
+
} else {
|
|
230
|
+
results = similarWines;
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
results = [];
|
|
234
|
+
console.error('[wineater] similar wines error:', error);
|
|
235
|
+
setSearchError();
|
|
236
|
+
} finally {
|
|
237
|
+
isLoading = false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
170
241
|
$effect(async () => {
|
|
171
242
|
// whenever results change, recompute translatedPopups asynchronously
|
|
172
243
|
if (!results || results.length === 0) {
|
|
@@ -200,6 +271,15 @@
|
|
|
200
271
|
});
|
|
201
272
|
}
|
|
202
273
|
|
|
274
|
+
function handleBuyLinkClick(wine: Wine | RecommendedWine, index: number) {
|
|
275
|
+
trackWidgetEvent(WIDGET_EVENTS.BUY_BUTTON_CLICKED, storeId, {
|
|
276
|
+
wine_id: wine.id,
|
|
277
|
+
wine_title: wine.title,
|
|
278
|
+
wine_url: wine.url,
|
|
279
|
+
is_mobile: window.innerWidth <= 768
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
203
283
|
function closePopup() {
|
|
204
284
|
const wine = results.find((w) => w.id === selectedWineId);
|
|
205
285
|
trackWidgetEvent(WIDGET_EVENTS.WINE_DETAILS_CLOSED, storeId, {
|
|
@@ -314,13 +394,22 @@
|
|
|
314
394
|
{isAnimating}
|
|
315
395
|
translatedPopup={translatedPopups[index]}
|
|
316
396
|
onBuyClick={handleBuyBtnClick}
|
|
397
|
+
onBuyLinkClick={handleBuyLinkClick}
|
|
317
398
|
onPopupClose={closePopup}
|
|
399
|
+
onSimilarClick={handleSimilarClick}
|
|
318
400
|
{onTileEnter}
|
|
319
401
|
{onTileLeave}
|
|
320
402
|
{onTileClick}
|
|
321
403
|
/>
|
|
322
404
|
{/each}
|
|
323
405
|
</div>
|
|
406
|
+
{#if hasMore}
|
|
407
|
+
<div class="wr-App-showMore">
|
|
408
|
+
<Action handleClick={handleShowMore} variant="outlined">
|
|
409
|
+
{getTranslation({ key: 'showMore', lang: appLanguage })}
|
|
410
|
+
</Action>
|
|
411
|
+
</div>
|
|
412
|
+
{/if}
|
|
324
413
|
{/if}
|
|
325
414
|
</div>
|
|
326
415
|
|
|
@@ -392,6 +481,12 @@
|
|
|
392
481
|
margin-bottom: 0;
|
|
393
482
|
}
|
|
394
483
|
|
|
484
|
+
.wr-App-showMore {
|
|
485
|
+
display: flex;
|
|
486
|
+
justify-content: center;
|
|
487
|
+
padding: 16px 0 24px;
|
|
488
|
+
}
|
|
489
|
+
|
|
395
490
|
/* ===================== 12) Error Message ===================== */
|
|
396
491
|
.wr-App-error {
|
|
397
492
|
text-align: center;
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
onTileLeave,
|
|
18
18
|
onTileClick,
|
|
19
19
|
onBuyClick,
|
|
20
|
+
onBuyLinkClick,
|
|
20
21
|
onPopupClose,
|
|
22
|
+
onSimilarClick,
|
|
21
23
|
isAnimating,
|
|
22
24
|
translatedPopup
|
|
23
25
|
}: {
|
|
@@ -29,7 +31,9 @@
|
|
|
29
31
|
onTileLeave: (wine: Wine | RecommendedWine) => void;
|
|
30
32
|
onTileClick: (event: MouseEvent, wine: Wine | RecommendedWine) => void;
|
|
31
33
|
onBuyClick: (wine: Wine | RecommendedWine, index: number) => void;
|
|
34
|
+
onBuyLinkClick: (wine: Wine | RecommendedWine, index: number) => void;
|
|
32
35
|
onPopupClose: () => void;
|
|
36
|
+
onSimilarClick?: (wine: Wine | RecommendedWine) => void;
|
|
33
37
|
isAnimating: boolean;
|
|
34
38
|
translatedPopup: Partial<RecommendedWine> | null;
|
|
35
39
|
} = $props();
|
|
@@ -99,6 +103,12 @@
|
|
|
99
103
|
</div>
|
|
100
104
|
{/if}
|
|
101
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
|
+
|
|
102
112
|
<h3 class="wr-WineTile-title">{wine.producer ? wine.producer : ''} {wine.title}</h3>
|
|
103
113
|
</header>
|
|
104
114
|
|
|
@@ -114,6 +124,7 @@
|
|
|
114
124
|
{#if wine.url}
|
|
115
125
|
<Action
|
|
116
126
|
href={wine.url}
|
|
127
|
+
handleClick={() => onBuyLinkClick(wine, index)}
|
|
117
128
|
className="wr-WineTile-action wr-WineTile-action--link js-WineTileAction"
|
|
118
129
|
target="_blank"
|
|
119
130
|
variant="outlined"
|
|
@@ -129,6 +140,15 @@
|
|
|
129
140
|
{getTranslation({ key: 'moreInfo', lang })}
|
|
130
141
|
</Action>
|
|
131
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}
|
|
132
152
|
</div>
|
|
133
153
|
</footer>
|
|
134
154
|
|
|
@@ -252,8 +272,26 @@
|
|
|
252
272
|
color: #333;
|
|
253
273
|
}
|
|
254
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
|
+
|
|
255
282
|
.wr-WineTile-actions {
|
|
256
283
|
margin: 16px auto 0;
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
align-items: center;
|
|
287
|
+
gap: 8px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.wr-WineTile-action--similar {
|
|
291
|
+
font-size: 13px;
|
|
292
|
+
color: var(--wr-brand-color, #7b2beb);
|
|
293
|
+
text-decoration: underline;
|
|
294
|
+
padding: 0;
|
|
257
295
|
}
|
|
258
296
|
|
|
259
297
|
/* 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
|
+
"showMore": "Plus de vins",
|
|
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
|
+
"showMore": "Ver más",
|
|
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
|
+
"showMore": "Show more",
|
|
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
|
+
"showMore": "Prikaži još",
|
|
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
|
+
"showMore": "Mehr anzeigen",
|
|
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
|
+
"showMore": "Показати ще",
|
|
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
|
+
"showMore": "Ещё варианты",
|
|
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
|
+
"showMore": "Тағы көрсету",
|
|
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
|
+
"showMore": "Mostrar mais",
|
|
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",
|