wineater-bot 4.24.1 → 4.24.2
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/package.json +5 -1
- package/.claude/settings.local.json +0 -10
- package/.prettierignore +0 -9
- package/.prettierrc +0 -16
- package/CLAUDE.md +0 -115
- package/docs/CUSTOMIZATION.md +0 -133
- package/docs/SVELTE.md +0 -146
- package/e2e/demo.test.ts +0 -6
- package/eslint.config.js +0 -41
- package/index.html +0 -23
- package/playwright.config.ts +0 -9
- package/pnpm-workspace.yaml +0 -2
- package/src/App.svelte +0 -784
- package/src/App.svelte.test.ts +0 -13
- package/src/assets/biobio.png +0 -0
- package/src/components/CookieConsent.svelte +0 -73
- package/src/components/Footer.svelte +0 -122
- package/src/components/Header.svelte +0 -99
- package/src/components/Search.svelte +0 -177
- package/src/components/SkeletonTiles.svelte +0 -94
- package/src/components/WineBottle.svelte +0 -56
- package/src/components/WinePopup.svelte +0 -192
- package/src/components/WineTile.svelte +0 -458
- package/src/components/icons/Budget.svelte +0 -35
- package/src/components/icons/Diamond.svelte +0 -35
- package/src/components/icons/Fish.svelte +0 -56
- package/src/components/icons/Heart.svelte +0 -28
- package/src/components/icons/Meat.svelte +0 -32
- package/src/components/ui/Action.svelte +0 -83
- package/src/demo.spec.ts +0 -13
- package/src/index.js +0 -7
- package/src/services/analytics/analytics.ts +0 -142
- package/src/services/analytics/constants.ts +0 -18
- package/src/services/api/api.ts +0 -298
- package/src/services/api/deserialize.ts +0 -120
- package/src/services/bottle/bottle.spec.ts +0 -32
- package/src/services/bottle/bottle.ts +0 -78
- package/src/services/cache.ts +0 -28
- package/src/services/i18n/i18n.ts +0 -146
- package/src/services/i18n/translations.json +0 -658
- package/src/services/profile/consent.svelte.ts +0 -115
- package/src/services/profile/events.ts +0 -73
- package/src/services/profile/uid.ts +0 -87
- package/src/services/shopify/cart.ts +0 -105
- package/src/services/shopify/shopify.spec.ts +0 -66
- package/src/services/shopify/shopify.ts +0 -150
- package/src/services/store/app-store.svelte.ts +0 -172
- package/src/services/store/deserialize.ts +0 -25
- package/src/services/store/types.ts +0 -12
- package/src/services/ui-config/get-config.ts +0 -25
- package/src/services/ui-config/projects-config.ts +0 -156
- package/src/services/ui-config/types.ts +0 -15
- package/src/services/ui-config/utils.ts +0 -40
- package/tsconfig.json +0 -23
- package/vite.config.js +0 -84
- package/vite.config.js.timestamp-1772121492941-e00587396dc48.mjs +0 -71
- package/vitest-setup-client.ts +0 -2
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { RecommendedWine, Wine } from './api.ts';
|
|
2
|
-
|
|
3
|
-
type RawInstantRecommendation = {
|
|
4
|
-
ID: string;
|
|
5
|
-
Appellation?: string;
|
|
6
|
-
Country?: string;
|
|
7
|
-
Grape: string;
|
|
8
|
-
Image: string;
|
|
9
|
-
Price: string;
|
|
10
|
-
Producer?: string;
|
|
11
|
-
Region: string;
|
|
12
|
-
Score?: string;
|
|
13
|
-
"Short Description"?: string;
|
|
14
|
-
Store: string;
|
|
15
|
-
Store_id?: string;
|
|
16
|
-
Tags: any[];
|
|
17
|
-
Title: string;
|
|
18
|
-
Type: string;
|
|
19
|
-
Url?: string;
|
|
20
|
-
Vintage?: string;
|
|
21
|
-
pairing_recommendation?: string;
|
|
22
|
-
Total?: string;
|
|
23
|
-
item_id?: string;
|
|
24
|
-
is_promote?: boolean;
|
|
25
|
-
stock?: string;
|
|
26
|
-
by_the_glass?: boolean;
|
|
27
|
-
glass_price?: string | number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function deserializeInstantRecommendation(raw: RawInstantRecommendation): Wine {
|
|
31
|
-
return {
|
|
32
|
-
id: String(raw.ID),
|
|
33
|
-
appellation: raw.Appellation || null,
|
|
34
|
-
country: raw.Country || null,
|
|
35
|
-
grape: raw.Grape,
|
|
36
|
-
image: raw.Image,
|
|
37
|
-
price: Number(raw.Price),
|
|
38
|
-
producer: raw.Producer || null,
|
|
39
|
-
region: raw.Region,
|
|
40
|
-
score: raw.Score ? Number(raw.Score) : null,
|
|
41
|
-
shortDescription: raw["Short Description"] || null,
|
|
42
|
-
store: raw.Store,
|
|
43
|
-
storeId: raw.Store_id || null,
|
|
44
|
-
tags: Array.isArray(raw.Tags) ? raw.Tags.map(String) : [],
|
|
45
|
-
title: raw.Title,
|
|
46
|
-
type: raw.Type,
|
|
47
|
-
url: raw.Url || null,
|
|
48
|
-
vintage: raw.Vintage ? Number(raw.Vintage) : null,
|
|
49
|
-
pairingRecommendation: raw.pairing_recommendation || null,
|
|
50
|
-
total: raw.Total ? Number(raw.Total) : null,
|
|
51
|
-
itemId: raw.item_id || null,
|
|
52
|
-
isPromote: raw.is_promote ? Boolean(raw.is_promote) : null,
|
|
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,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
type RawRecommendedWine = {
|
|
60
|
-
id: string;
|
|
61
|
-
title: string;
|
|
62
|
-
description: string;
|
|
63
|
-
region: string;
|
|
64
|
-
price: string;
|
|
65
|
-
grape: string;
|
|
66
|
-
appellation?: string;
|
|
67
|
-
url?: string;
|
|
68
|
-
store: string;
|
|
69
|
-
storeId?: string;
|
|
70
|
-
image: string;
|
|
71
|
-
country?: string;
|
|
72
|
-
vintage?: string;
|
|
73
|
-
tags: any[];
|
|
74
|
-
score?: string;
|
|
75
|
-
total?: string;
|
|
76
|
-
is_promote?: boolean;
|
|
77
|
-
stock?: string;
|
|
78
|
-
distance: string;
|
|
79
|
-
reason: string;
|
|
80
|
-
type: string;
|
|
81
|
-
by_the_glass?: boolean;
|
|
82
|
-
glass_price?: string | number;
|
|
83
|
-
}
|
|
84
|
-
export function deserializeRecommendedWine(raw: RawRecommendedWine): RecommendedWine {
|
|
85
|
-
return {
|
|
86
|
-
id: String(raw.id),
|
|
87
|
-
title: String(raw.title),
|
|
88
|
-
description: String(raw.description),
|
|
89
|
-
region: String(raw.region),
|
|
90
|
-
price: Number(raw.price),
|
|
91
|
-
grape: String(raw.grape),
|
|
92
|
-
appellation: raw.appellation ? String(raw.appellation) : null,
|
|
93
|
-
url: raw.url ? String(raw.url) : null,
|
|
94
|
-
store: String(raw.store),
|
|
95
|
-
storeId: raw.storeId ? String(raw.storeId) : null,
|
|
96
|
-
image: String(raw.image),
|
|
97
|
-
country: raw.country ? String(raw.country) : null,
|
|
98
|
-
vintage: raw.vintage ? Number(raw.vintage) : null,
|
|
99
|
-
tags: Array.isArray(raw.tags) ? raw.tags.map(String) : [],
|
|
100
|
-
score: raw.score ? Number(raw.score) : null,
|
|
101
|
-
total: raw.total ? Number(raw.total) : null,
|
|
102
|
-
isPromote: raw.is_promote ? Boolean(raw.is_promote) : null,
|
|
103
|
-
stock: raw.stock ? Number(raw.stock) : null,
|
|
104
|
-
distance: Number(raw.distance),
|
|
105
|
-
reason: String(raw.reason),
|
|
106
|
-
type: String(raw.type),
|
|
107
|
-
byTheGlass: Boolean(raw.by_the_glass),
|
|
108
|
-
glassPrice: raw.glass_price != null ? Number(raw.glass_price) : null,
|
|
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
|
-
};
|
|
120
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { bottleKind, hasImage } from './bottle';
|
|
3
|
-
|
|
4
|
-
describe('bottleKind', () => {
|
|
5
|
-
it('maps the types the API sends', () => {
|
|
6
|
-
expect(bottleKind('Red')).toBe('red');
|
|
7
|
-
expect(bottleKind('Rosé')).toBe('rose');
|
|
8
|
-
expect(bottleKind('Rose')).toBe('rose');
|
|
9
|
-
expect(bottleKind('Sparkling')).toBe('sparkling');
|
|
10
|
-
expect(bottleKind('Champagne brut')).toBe('sparkling');
|
|
11
|
-
expect(bottleKind('sweet')).toBe('sweet');
|
|
12
|
-
expect(bottleKind('Port')).toBe('fortified');
|
|
13
|
-
expect(bottleKind('fortified')).toBe('fortified');
|
|
14
|
-
});
|
|
15
|
-
it('falls back to white', () => {
|
|
16
|
-
expect(bottleKind('White')).toBe('white');
|
|
17
|
-
expect(bottleKind('')).toBe('white');
|
|
18
|
-
expect(bottleKind(null)).toBe('white');
|
|
19
|
-
expect(bottleKind('orange')).toBe('white');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('hasImage', () => {
|
|
24
|
-
it('rejects empty and stringified nulls', () => {
|
|
25
|
-
expect(hasImage('')).toBe(false);
|
|
26
|
-
expect(hasImage(' ')).toBe(false);
|
|
27
|
-
expect(hasImage('null')).toBe(false);
|
|
28
|
-
expect(hasImage('undefined')).toBe(false);
|
|
29
|
-
expect(hasImage(null)).toBe(false);
|
|
30
|
-
expect(hasImage('https://cdn.example/a.png')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fallback bottle drawn from the wine type, used when a wine has no image or its image fails to load.
|
|
3
|
-
* Pure data: the same type always gives the same drawing. Keep in sync with the admin onboarding
|
|
4
|
-
* (wineater-admin-panel/src/pages/OnboardingPage.tsx), which draws the same bottles.
|
|
5
|
-
*/
|
|
6
|
-
export type Shoulder = 'bordeaux' | 'burgundy' | 'flute' | 'champagne' | 'port';
|
|
7
|
-
export type BottleKind = 'red' | 'white' | 'rose' | 'sparkling' | 'sweet' | 'fortified';
|
|
8
|
-
|
|
9
|
-
export const BOTTLES: Record<BottleKind, { glass: string; foil: string; accent: string; shoulder: Shoulder }> = {
|
|
10
|
-
red: { glass: '#3b1230', foil: '#7a1f3d', accent: '#7a1f3d', shoulder: 'bordeaux' },
|
|
11
|
-
white: { glass: '#a9b568', foil: '#c9b458', accent: '#8a8f3c', shoulder: 'flute' },
|
|
12
|
-
rose: { glass: '#d98a9d', foil: '#e8a3b4', accent: '#b8566f', shoulder: 'burgundy' },
|
|
13
|
-
sparkling: { glass: '#5f6b3a', foil: '#d9c26a', accent: '#b89b2e', shoulder: 'champagne' },
|
|
14
|
-
sweet: { glass: '#b8862e', foil: '#d9a441', accent: '#9a6a1c', shoulder: 'flute' },
|
|
15
|
-
fortified: { glass: '#4a2a1a', foil: '#8a5a2b', accent: '#8a5a2b', shoulder: 'port' },
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type Box = { x: number; y: number; w: number; h: number };
|
|
19
|
-
|
|
20
|
-
/** Silhouettes on a 44x130 canvas; every bottle spans y=2..123. */
|
|
21
|
-
export const SHAPES: Record<Shoulder, { body: string; cap: Box; label: Box; shine: string }> = {
|
|
22
|
-
flute: {
|
|
23
|
-
body: 'M17 4h10v34c0 6 8 10 8 24v56c0 3-2 5-5 5H14c-3 0-5-2-5-5V62c0-14 8-18 8-24z',
|
|
24
|
-
cap: { x: 16, y: 2, w: 12, h: 20 },
|
|
25
|
-
label: { x: 9, y: 72, w: 26, h: 34 },
|
|
26
|
-
shine: 'M12 66c0 18 0 40 0 52',
|
|
27
|
-
},
|
|
28
|
-
burgundy: {
|
|
29
|
-
body: 'M17 4h10v30c0 10 11 14 11 30v54c0 3-2 5-5 5H11c-3 0-5-2-5-5V64c0-16 11-20 11-30z',
|
|
30
|
-
cap: { x: 16, y: 2, w: 12, h: 20 },
|
|
31
|
-
label: { x: 8, y: 72, w: 28, h: 34 },
|
|
32
|
-
shine: 'M11 68c0 18 0 38 0 50',
|
|
33
|
-
},
|
|
34
|
-
bordeaux: {
|
|
35
|
-
body: 'M17 4h10v32c0 4 1 6 4 9 4 4 6 7 6 14v59c0 3-2 5-5 5H12c-3 0-5-2-5-5V59c0-7 2-10 6-14 3-3 4-5 4-9z',
|
|
36
|
-
cap: { x: 16, y: 2, w: 12, h: 20 },
|
|
37
|
-
label: { x: 9, y: 72, w: 26, h: 34 },
|
|
38
|
-
shine: 'M12 64c0 18 0 40 0 54',
|
|
39
|
-
},
|
|
40
|
-
champagne: {
|
|
41
|
-
body: 'M17 4h10v34c0 12 15 18 15 42v35c0 5-3 8-7 8H9c-4 0-7-3-7-8V80c0-24 15-30 15-42z',
|
|
42
|
-
cap: { x: 15, y: 2, w: 14, h: 34 },
|
|
43
|
-
label: { x: 6, y: 78, w: 32, h: 36 },
|
|
44
|
-
shine: 'M8 82c0 14 0 28 0 36',
|
|
45
|
-
},
|
|
46
|
-
port: {
|
|
47
|
-
body: 'M16 4h12v16c0 3-2 4-2 7s3 4 3 8c0 6 9 6 9 18v65c0 3-2 5-5 5H11c-3 0-5-2-5-5V53c0-12 9-12 9-18 0-4 3-5 3-8s-2-4-2-7z',
|
|
48
|
-
cap: { x: 15, y: 2, w: 14, h: 15 },
|
|
49
|
-
label: { x: 8, y: 66, w: 28, h: 42 },
|
|
50
|
-
shine: 'M11 60c0 20 0 40 0 56',
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
/** Bubbles of the sparkling bottle: cx, cy, r. */
|
|
55
|
-
export const BUBBLES: [number, number, number][] = [
|
|
56
|
-
[20, 58, 1.5], [26, 50, 1.1], [23, 44, 0.8], [14, 66, 1.2], [30, 63, 1.7], [9, 86, 1.1],
|
|
57
|
-
[34, 92, 1.3], [8, 106, 1.6], [35, 112, 1], [25, 70, 0.9], [17, 73, 0.7],
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
/** "Red", "Rosé", "Rouge", "Champagne", "dessert wine", ... -> one of six kinds; unknown -> white. */
|
|
61
|
-
export function bottleKind(type: string | null | undefined): BottleKind {
|
|
62
|
-
const v = (type ?? '')
|
|
63
|
-
.toLowerCase()
|
|
64
|
-
.normalize('NFD')
|
|
65
|
-
.replace(/[̀-ͯ]/g, '');
|
|
66
|
-
if (/(spark|champ|cava|prosecco|cremant|brut|pet.?nat|espum|petillant)/.test(v)) return 'sparkling';
|
|
67
|
-
if (/(rose|pink|rosado)/.test(v)) return 'rose';
|
|
68
|
-
if (/(fortif|port|sherry|madeira|marsala)/.test(v)) return 'fortified';
|
|
69
|
-
if (/(sweet|dessert|late|sauternes|ice|moelleux|dulce)/.test(v)) return 'sweet';
|
|
70
|
-
if (/(red|rouge|tinto|rosso|krasn)/.test(v)) return 'red';
|
|
71
|
-
return 'white';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** A usable image URL: the API sometimes sends "", "null" or "undefined" as a string. */
|
|
75
|
-
export function hasImage(src: string | null | undefined): src is string {
|
|
76
|
-
const s = (src ?? '').trim();
|
|
77
|
-
return !!s && s !== 'null' && s !== 'undefined';
|
|
78
|
-
}
|
package/src/services/cache.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// Tiny stale-while-revalidate cache backed by localStorage.
|
|
2
|
-
// Storage can throw or be unavailable (private mode, blocked cookies, embeds),
|
|
3
|
-
// so every access is wrapped and the widget works the same without it.
|
|
4
|
-
|
|
5
|
-
const PREFIX = 'wineater:v1:';
|
|
6
|
-
|
|
7
|
-
type Entry<T> = { savedAt: number; value: T };
|
|
8
|
-
|
|
9
|
-
export function readCache<T>(key: string, maxAgeMs: number): T | null {
|
|
10
|
-
try {
|
|
11
|
-
const raw = window.localStorage.getItem(PREFIX + key);
|
|
12
|
-
if (!raw) return null;
|
|
13
|
-
const entry = JSON.parse(raw) as Entry<T>;
|
|
14
|
-
if (Date.now() - entry.savedAt > maxAgeMs) return null;
|
|
15
|
-
return entry.value;
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function writeCache<T>(key: string, value: T): void {
|
|
22
|
-
try {
|
|
23
|
-
const entry: Entry<T> = { savedAt: Date.now(), value };
|
|
24
|
-
window.localStorage.setItem(PREFIX + key, JSON.stringify(entry));
|
|
25
|
-
} catch {
|
|
26
|
-
// quota exceeded or storage disabled: caching is best-effort
|
|
27
|
-
}
|
|
28
|
-
}
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import translationsJson from './translations.json';
|
|
2
|
-
// TODO: use DI to get custom translations to avoid high coupling
|
|
3
|
-
import { appStore } from '../store/app-store.svelte';
|
|
4
|
-
|
|
5
|
-
export type LanguageCode = keyof typeof translationsJson;
|
|
6
|
-
|
|
7
|
-
export type CurrencyCode = string;
|
|
8
|
-
|
|
9
|
-
const MAP_LOCALE_TO_ISO_CODE: Record<string, CurrencyCode> = {
|
|
10
|
-
'en': 'en-US',
|
|
11
|
-
'de': 'de-DE',
|
|
12
|
-
'fr': 'fr-FR',
|
|
13
|
-
'es': 'es-ES',
|
|
14
|
-
'it': 'it-IT',
|
|
15
|
-
'ja': 'ja-JP',
|
|
16
|
-
'zh': 'zh-CN',
|
|
17
|
-
'ru': 'ru-RU',
|
|
18
|
-
'kk': 'kk-KZ',
|
|
19
|
-
'pt': 'pt-PT',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function formatCurrency(amount: number, currencyCode: CurrencyCode, locale = "en"): string {
|
|
23
|
-
if (!currencyCode) {
|
|
24
|
-
return amount.toString();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
return new Intl.NumberFormat(MAP_LOCALE_TO_ISO_CODE[locale] || "en-US", {
|
|
29
|
-
style: 'currency',
|
|
30
|
-
currency: currencyCode,
|
|
31
|
-
currencyDisplay: 'narrowSymbol'
|
|
32
|
-
}).format(amount);
|
|
33
|
-
} catch {
|
|
34
|
-
return amount.toString();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// nested translations are not supported yet
|
|
39
|
-
// in translations.json use flat keys with dots instead of nested objects
|
|
40
|
-
// yes: "product.description.short": "..."
|
|
41
|
-
// no: "product": { "description": { "short": "..." } }
|
|
42
|
-
export function getTranslation({ key, lang }): string {
|
|
43
|
-
const translations = translationsJson as Partial<Record<LanguageCode, Record<string, string>>>;
|
|
44
|
-
const appState = appStore.getUiConfig();
|
|
45
|
-
const projectTranslations = appState?.translations || {};
|
|
46
|
-
if (projectTranslations[lang] && projectTranslations[lang][key]) {
|
|
47
|
-
return projectTranslations[lang][key];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (translations[lang] && translations[lang][key]) {
|
|
51
|
-
return translations[lang][key];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
throw new Error(`Translation for key "${key}" not found in language "${lang}".`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type TranslationEntry = {
|
|
58
|
-
key: string;
|
|
59
|
-
text: string;
|
|
60
|
-
};
|
|
61
|
-
type TranslateTextsArguments = {
|
|
62
|
-
sourceLang: string;
|
|
63
|
-
targetLang: string;
|
|
64
|
-
texts: Record<string, string>;
|
|
65
|
-
apiKey: string | undefined;
|
|
66
|
-
};
|
|
67
|
-
export async function translateTexts<T extends Record<string, string>>({ sourceLang, targetLang, texts, apiKey }: TranslateTextsArguments): Promise<T> {
|
|
68
|
-
if (!apiKey) return texts;
|
|
69
|
-
if (!sourceLang || !targetLang || sourceLang === targetLang) return texts;
|
|
70
|
-
|
|
71
|
-
const url = 'https://api.openai.com/v1/chat/completions';
|
|
72
|
-
|
|
73
|
-
const headers = {
|
|
74
|
-
'Content-Type': 'application/json',
|
|
75
|
-
Authorization: `Bearer ${apiKey}`,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const entries: TranslationEntry[] = Object.entries(texts).map(([key, text]) => ({
|
|
79
|
-
key,
|
|
80
|
-
text,
|
|
81
|
-
}));
|
|
82
|
-
|
|
83
|
-
const prompt = `
|
|
84
|
-
You are a professional translator.
|
|
85
|
-
|
|
86
|
-
The input texts are written in language with code "${sourceLang}". Your task is to translate them strictly to the target language with code "${targetLang}".
|
|
87
|
-
|
|
88
|
-
⚠️ DO NOT leave the original text.
|
|
89
|
-
⚠️ DO NOT skip any entry.
|
|
90
|
-
⚠️ Preserve the original order and keys exactly.
|
|
91
|
-
|
|
92
|
-
Translate only the "text" fields of the JSON array. Return an array of the same length and structure, like this:
|
|
93
|
-
|
|
94
|
-
Input:
|
|
95
|
-
[
|
|
96
|
-
{ "key": "shortDescription", "text": "Red wine with rich cherry notes." },
|
|
97
|
-
{ "key": "tags", "text": "fruity, dry, elegant" }
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
Output:
|
|
101
|
-
[
|
|
102
|
-
{ "key": "shortDescription", "text": "..." },
|
|
103
|
-
{ "key": "tags", "text": "..." }
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
Now translate the following input:
|
|
107
|
-
${JSON.stringify(entries, null, 2)}
|
|
108
|
-
`;
|
|
109
|
-
|
|
110
|
-
const body = JSON.stringify({
|
|
111
|
-
model: 'gpt-3.5-turbo',
|
|
112
|
-
messages: [
|
|
113
|
-
{ role: 'system', content: 'You are a professional translator.' },
|
|
114
|
-
{ role: 'user', content: prompt },
|
|
115
|
-
],
|
|
116
|
-
temperature: 0.2,
|
|
117
|
-
max_tokens: 1000,
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const response = await fetch(url, {
|
|
122
|
-
method: 'POST',
|
|
123
|
-
headers,
|
|
124
|
-
body,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const data = await response.json();
|
|
128
|
-
const translatedRaw: string | undefined = data?.choices?.[0]?.message?.content?.trim();
|
|
129
|
-
|
|
130
|
-
if (!translatedRaw) throw new Error('No response from OpenAI');
|
|
131
|
-
|
|
132
|
-
const translatedArray: TranslationEntry[] = JSON.parse(translatedRaw);
|
|
133
|
-
|
|
134
|
-
const translatedObject: Record<string, string> = {};
|
|
135
|
-
translatedArray.forEach((entry) => {
|
|
136
|
-
if (entry.key && entry.text !== undefined) {
|
|
137
|
-
translatedObject[entry.key] = entry.text;
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
return { ...texts, ...translatedObject } as T;
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.error('Error translating texts:', error);
|
|
144
|
-
return texts;
|
|
145
|
-
}
|
|
146
|
-
}
|