@giovchat/react-native 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/GiovChat.d.ts +100 -0
- package/dist/GiovChat.d.ts.map +1 -0
- package/dist/GiovChat.js +257 -0
- package/dist/GiovChat.js.map +1 -0
- package/dist/attachments.d.ts +121 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +185 -0
- package/dist/attachments.js.map +1 -0
- package/dist/brand-assets.d.ts +6 -0
- package/dist/brand-assets.d.ts.map +1 -0
- package/dist/brand-assets.js +9 -0
- package/dist/brand-assets.js.map +1 -0
- package/dist/client.d.ts +190 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +801 -0
- package/dist/client.js.map +1 -0
- package/dist/color.d.ts +25 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +78 -0
- package/dist/color.js.map +1 -0
- package/dist/components/Attachments.d.ts +13 -0
- package/dist/components/Attachments.d.ts.map +1 -0
- package/dist/components/Attachments.js +42 -0
- package/dist/components/Attachments.js.map +1 -0
- package/dist/components/Branding.d.ts +6 -0
- package/dist/components/Branding.d.ts.map +1 -0
- package/dist/components/Branding.js +28 -0
- package/dist/components/Branding.js.map +1 -0
- package/dist/components/ChatMarkdown.d.ts +15 -0
- package/dist/components/ChatMarkdown.d.ts.map +1 -0
- package/dist/components/ChatMarkdown.js +72 -0
- package/dist/components/ChatMarkdown.js.map +1 -0
- package/dist/components/GiovBotMark.d.ts +6 -0
- package/dist/components/GiovBotMark.d.ts.map +1 -0
- package/dist/components/GiovBotMark.js +11 -0
- package/dist/components/GiovBotMark.js.map +1 -0
- package/dist/components/GreetingBubble.d.ts +7 -0
- package/dist/components/GreetingBubble.d.ts.map +1 -0
- package/dist/components/GreetingBubble.js +12 -0
- package/dist/components/GreetingBubble.js.map +1 -0
- package/dist/components/Header.d.ts +18 -0
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.js +67 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/MessageBubble.d.ts +16 -0
- package/dist/components/MessageBubble.d.ts.map +1 -0
- package/dist/components/MessageBubble.js +49 -0
- package/dist/components/MessageBubble.js.map +1 -0
- package/dist/components/PreChatForm.d.ts +11 -0
- package/dist/components/PreChatForm.d.ts.map +1 -0
- package/dist/components/PreChatForm.js +72 -0
- package/dist/components/PreChatForm.js.map +1 -0
- package/dist/components/TypingIndicator.d.ts +9 -0
- package/dist/components/TypingIndicator.d.ts.map +1 -0
- package/dist/components/TypingIndicator.js +18 -0
- package/dist/components/TypingIndicator.js.map +1 -0
- package/dist/components/WidgetStyles.d.ts +3 -0
- package/dist/components/WidgetStyles.d.ts.map +1 -0
- package/dist/components/WidgetStyles.js +9 -0
- package/dist/components/WidgetStyles.js.map +1 -0
- package/dist/components/bubbleStyles.d.ts +47 -0
- package/dist/components/bubbleStyles.d.ts.map +1 -0
- package/dist/components/bubbleStyles.js +19 -0
- package/dist/components/bubbleStyles.js.map +1 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +14 -0
- package/dist/components/index.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/i18n.d.ts +73 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +292 -0
- package/dist/i18n.js.map +1 -0
- package/dist/identity.d.ts +41 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +41 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +56 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +219 -0
- package/dist/markdown.js.map +1 -0
- package/dist/outbox.d.ts +68 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +168 -0
- package/dist/outbox.js.map +1 -0
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +34 -0
- package/dist/protocol.js.map +1 -0
- package/dist/styleOverrides.d.ts +37 -0
- package/dist/styleOverrides.d.ts.map +1 -0
- package/dist/styleOverrides.js +54 -0
- package/dist/styleOverrides.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/unread.d.ts +9 -0
- package/dist/unread.d.ts.map +1 -0
- package/dist/unread.js +49 -0
- package/dist/unread.js.map +1 -0
- package/dist/useChatSession.d.ts +78 -0
- package/dist/useChatSession.d.ts.map +1 -0
- package/dist/useChatSession.js +796 -0
- package/dist/useChatSession.js.map +1 -0
- package/package.json +59 -4
- package/src/GiovChat.tsx +530 -0
- package/src/attachments.ts +218 -0
- package/src/brand-assets.ts +5 -0
- package/src/client.ts +758 -0
- package/src/color.ts +74 -0
- package/src/components/Attachments.tsx +75 -0
- package/src/components/Branding.tsx +35 -0
- package/src/components/ChatMarkdown.tsx +111 -0
- package/src/components/GiovBotMark.tsx +19 -0
- package/src/components/GreetingBubble.tsx +20 -0
- package/src/components/Header.tsx +131 -0
- package/src/components/MessageBubble.tsx +121 -0
- package/src/components/PreChatForm.tsx +183 -0
- package/src/components/TypingIndicator.tsx +34 -0
- package/src/components/WidgetStyles.tsx +5 -0
- package/src/components/bubbleStyles.ts +16 -0
- package/src/components/index.ts +14 -0
- package/src/errors.ts +30 -0
- package/src/i18n.ts +351 -0
- package/src/identity.ts +54 -0
- package/src/index.ts +68 -0
- package/src/markdown.ts +227 -0
- package/src/outbox.ts +175 -0
- package/src/protocol.ts +31 -0
- package/src/styleOverrides.ts +85 -0
- package/src/types.ts +119 -0
- package/src/unread.ts +39 -0
- package/src/useChatSession.ts +695 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ScrollView,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
Text,
|
|
6
|
+
TextInput,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
View,
|
|
9
|
+
} from "react-native";
|
|
10
|
+
import type { GiovChatContact, RequestUserInfo } from "../types";
|
|
11
|
+
import { useWidgetStyles } from "./WidgetStyles";
|
|
12
|
+
import { useGiovChatStrings } from "../i18n";
|
|
13
|
+
import { textOn } from "../color";
|
|
14
|
+
|
|
15
|
+
export interface PreChatFormProps {
|
|
16
|
+
fields: RequestUserInfo;
|
|
17
|
+
primaryColor: string;
|
|
18
|
+
dark: boolean;
|
|
19
|
+
initialValues?: GiovChatContact;
|
|
20
|
+
onSubmit: (info: GiovChatContact) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
24
|
+
|
|
25
|
+
export function PreChatForm({
|
|
26
|
+
fields,
|
|
27
|
+
primaryColor,
|
|
28
|
+
dark,
|
|
29
|
+
initialValues,
|
|
30
|
+
onSubmit,
|
|
31
|
+
}: PreChatFormProps) {
|
|
32
|
+
const custom = useWidgetStyles();
|
|
33
|
+
const strings = useGiovChatStrings();
|
|
34
|
+
const [firstName, setFirstName] = useState(initialValues?.firstName ?? "");
|
|
35
|
+
const [lastName, setLastName] = useState(initialValues?.lastName ?? "");
|
|
36
|
+
const [email, setEmail] = useState(initialValues?.email ?? "");
|
|
37
|
+
const [phone, setPhone] = useState(initialValues?.phone ?? "");
|
|
38
|
+
|
|
39
|
+
const bg = dark ? "#111" : "#fff";
|
|
40
|
+
const textColor = dark ? "#f0f0f0" : "#111";
|
|
41
|
+
const labelColor = dark ? "#aaa" : "#555";
|
|
42
|
+
const inputBg = dark ? "#1c1c1c" : "#fafafa";
|
|
43
|
+
const inputBorder = dark ? "#333" : "#e5e5e5";
|
|
44
|
+
const placeholderColor = custom.hint.color ?? (dark ? "#555" : "#c0c0c0");
|
|
45
|
+
|
|
46
|
+
const isValid =
|
|
47
|
+
(!fields.firstName || firstName.trim().length > 0) &&
|
|
48
|
+
(!fields.lastName || lastName.trim().length > 0) &&
|
|
49
|
+
(!fields.email || EMAIL_RE.test(email.trim())) &&
|
|
50
|
+
(!fields.phone || phone.trim().length > 0);
|
|
51
|
+
|
|
52
|
+
const handleSubmit = () => {
|
|
53
|
+
if (!isValid) return;
|
|
54
|
+
onSubmit({
|
|
55
|
+
firstName: firstName.trim() || undefined,
|
|
56
|
+
lastName: lastName.trim() || undefined,
|
|
57
|
+
email: email.trim() || undefined,
|
|
58
|
+
phone: phone.trim() || undefined,
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const inputStyle = [
|
|
63
|
+
styles.input,
|
|
64
|
+
{ color: textColor, backgroundColor: inputBg, borderColor: inputBorder },
|
|
65
|
+
custom.input,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<ScrollView
|
|
70
|
+
style={[styles.scroll, { backgroundColor: bg }, custom.form]}
|
|
71
|
+
contentContainerStyle={styles.content}
|
|
72
|
+
keyboardShouldPersistTaps="handled"
|
|
73
|
+
>
|
|
74
|
+
<Text style={[styles.heading, { color: textColor }, custom.formTitle]}>{strings.preChatTitle}</Text>
|
|
75
|
+
|
|
76
|
+
{fields.firstName && (
|
|
77
|
+
<View style={styles.field}>
|
|
78
|
+
<Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.firstName)}</Text>
|
|
79
|
+
<TextInput
|
|
80
|
+
style={inputStyle}
|
|
81
|
+
value={firstName}
|
|
82
|
+
accessibilityLabel={strings.required(strings.firstName)}
|
|
83
|
+
onChangeText={setFirstName}
|
|
84
|
+
placeholder={strings.firstName}
|
|
85
|
+
placeholderTextColor={placeholderColor}
|
|
86
|
+
autoCapitalize="words"
|
|
87
|
+
autoCorrect={false}
|
|
88
|
+
returnKeyType="next"
|
|
89
|
+
/>
|
|
90
|
+
</View>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{fields.lastName && (
|
|
94
|
+
<View style={styles.field}>
|
|
95
|
+
<Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.lastName)}</Text>
|
|
96
|
+
<TextInput
|
|
97
|
+
style={inputStyle}
|
|
98
|
+
value={lastName}
|
|
99
|
+
accessibilityLabel={strings.required(strings.lastName)}
|
|
100
|
+
onChangeText={setLastName}
|
|
101
|
+
placeholder={strings.lastName}
|
|
102
|
+
placeholderTextColor={placeholderColor}
|
|
103
|
+
autoCapitalize="words"
|
|
104
|
+
autoCorrect={false}
|
|
105
|
+
returnKeyType="next"
|
|
106
|
+
/>
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
{fields.email && (
|
|
111
|
+
<View style={styles.field}>
|
|
112
|
+
<Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.email)}</Text>
|
|
113
|
+
<TextInput
|
|
114
|
+
style={inputStyle}
|
|
115
|
+
value={email}
|
|
116
|
+
accessibilityLabel={strings.required(strings.email)}
|
|
117
|
+
onChangeText={setEmail}
|
|
118
|
+
placeholder="you@example.com"
|
|
119
|
+
placeholderTextColor={placeholderColor}
|
|
120
|
+
keyboardType="email-address"
|
|
121
|
+
autoCapitalize="none"
|
|
122
|
+
autoCorrect={false}
|
|
123
|
+
returnKeyType="next"
|
|
124
|
+
/>
|
|
125
|
+
</View>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{fields.phone && (
|
|
129
|
+
<View style={styles.field}>
|
|
130
|
+
<Text style={[styles.label, { color: labelColor }, custom.label]}>{strings.required(strings.phone)}</Text>
|
|
131
|
+
<TextInput
|
|
132
|
+
style={inputStyle}
|
|
133
|
+
value={phone}
|
|
134
|
+
accessibilityLabel={strings.required(strings.phone)}
|
|
135
|
+
onChangeText={setPhone}
|
|
136
|
+
placeholder="+1 555 000 0000"
|
|
137
|
+
placeholderTextColor={placeholderColor}
|
|
138
|
+
keyboardType="phone-pad"
|
|
139
|
+
returnKeyType="done"
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
)}
|
|
143
|
+
|
|
144
|
+
<TouchableOpacity
|
|
145
|
+
style={[
|
|
146
|
+
styles.submitBtn,
|
|
147
|
+
{ backgroundColor: primaryColor },
|
|
148
|
+
custom.button,
|
|
149
|
+
!isValid && styles.submitBtnDisabled,
|
|
150
|
+
]}
|
|
151
|
+
onPress={handleSubmit}
|
|
152
|
+
disabled={!isValid}
|
|
153
|
+
accessibilityRole="button"
|
|
154
|
+
accessibilityLabel={strings.startChat}
|
|
155
|
+
>
|
|
156
|
+
<Text style={[styles.submitBtnText, { color: textOn(custom.button.backgroundColor ?? primaryColor) }, custom.buttonText]}>{strings.startChat}</Text>
|
|
157
|
+
</TouchableOpacity>
|
|
158
|
+
</ScrollView>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const styles = StyleSheet.create({
|
|
163
|
+
scroll: { flex: 1 },
|
|
164
|
+
content: { padding: 20, flexGrow: 1 },
|
|
165
|
+
heading: { fontSize: 18, fontWeight: "700", marginBottom: 24 },
|
|
166
|
+
field: { marginBottom: 16 },
|
|
167
|
+
label: { fontSize: 13, fontWeight: "600", marginBottom: 6 },
|
|
168
|
+
input: {
|
|
169
|
+
borderWidth: 1,
|
|
170
|
+
borderRadius: 10,
|
|
171
|
+
paddingHorizontal: 14,
|
|
172
|
+
paddingVertical: 11,
|
|
173
|
+
fontSize: 14,
|
|
174
|
+
},
|
|
175
|
+
submitBtn: {
|
|
176
|
+
borderRadius: 10,
|
|
177
|
+
paddingVertical: 14,
|
|
178
|
+
alignItems: "center",
|
|
179
|
+
marginTop: 8,
|
|
180
|
+
},
|
|
181
|
+
submitBtnDisabled: { opacity: 0.45 },
|
|
182
|
+
submitBtnText: { color: "#fff", fontWeight: "700", fontSize: 15 },
|
|
183
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text, View } from "react-native";
|
|
3
|
+
import { bubbleStyles as s } from "./bubbleStyles";
|
|
4
|
+
import { useWidgetStyles } from "./WidgetStyles";
|
|
5
|
+
import { GiovBotMark } from "./GiovBotMark";
|
|
6
|
+
import { useGiovChatStrings } from "../i18n";
|
|
7
|
+
|
|
8
|
+
export interface TypingIndicatorProps {
|
|
9
|
+
dark: boolean;
|
|
10
|
+
/** Only explicit server AI identity enables the AI mark. */
|
|
11
|
+
isBot?: boolean;
|
|
12
|
+
botName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function TypingIndicator({ dark, isBot, botName }: TypingIndicatorProps) {
|
|
16
|
+
const custom = useWidgetStyles();
|
|
17
|
+
const strings = useGiovChatStrings();
|
|
18
|
+
const isAI = isBot === true;
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
style={[s.wrapper, s.left, isAI && s.assistantRow]}
|
|
22
|
+
// One element, so screen readers say the label instead of the dots (iOS ignores a label otherwise).
|
|
23
|
+
accessible
|
|
24
|
+
accessibilityLabel={isAI ? strings.botTyping(botName?.trim() || "GiovBot") : strings.teamTyping}
|
|
25
|
+
>
|
|
26
|
+
{isAI && <GiovBotMark />}
|
|
27
|
+
<View style={[s.bubble, s.agentBubble, dark && s.agentBubbleDark, custom.agentBubble]}>
|
|
28
|
+
<Text style={[{ color: dark ? "#666" : "#aaa", fontSize: 20, letterSpacing: 4 }, custom.agentText]}>
|
|
29
|
+
• • •
|
|
30
|
+
</Text>
|
|
31
|
+
</View>
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
|
|
3
|
+
/** Shared styles for chat bubble components. */
|
|
4
|
+
export const bubbleStyles = StyleSheet.create({
|
|
5
|
+
wrapper: { marginHorizontal: 16, marginBottom: 8 },
|
|
6
|
+
right: { alignItems: "flex-end" },
|
|
7
|
+
left: { alignItems: "flex-start" },
|
|
8
|
+
assistantRow: { flexDirection: "row", gap: 8 },
|
|
9
|
+
bubble: { maxWidth: "78%", borderRadius: 16, padding: 12 },
|
|
10
|
+
visitorBubble: { borderBottomRightRadius: 4 },
|
|
11
|
+
agentBubble: { backgroundColor: "#f0f0f0", borderBottomLeftRadius: 4 },
|
|
12
|
+
agentBubbleDark: { backgroundColor: "#242424" },
|
|
13
|
+
sender: { fontSize: 11, fontWeight: "600", marginBottom: 3 },
|
|
14
|
+
content: { fontSize: 14, lineHeight: 20 },
|
|
15
|
+
timestamp: { fontSize: 10, marginTop: 4, textAlign: "right" },
|
|
16
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Header } from "./Header";
|
|
2
|
+
export type { HeaderProps } from "./Header";
|
|
3
|
+
|
|
4
|
+
export { MessageBubble } from "./MessageBubble";
|
|
5
|
+
export type { MessageBubbleProps } from "./MessageBubble";
|
|
6
|
+
|
|
7
|
+
export { GreetingBubble } from "./GreetingBubble";
|
|
8
|
+
export type { GreetingBubbleProps } from "./GreetingBubble";
|
|
9
|
+
|
|
10
|
+
export { TypingIndicator } from "./TypingIndicator";
|
|
11
|
+
export type { TypingIndicatorProps } from "./TypingIndicator";
|
|
12
|
+
|
|
13
|
+
export { PreChatForm } from "./PreChatForm";
|
|
14
|
+
export type { PreChatFormProps } from "./PreChatForm";
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type GiovChatErrorCode =
|
|
2
|
+
| "network" | "timeout" | "invalid_response" | "invalid_session" | "conversation_closed"
|
|
3
|
+
| "connection_failed" | "disconnected" | "send_failed" | "upload_failed" | "workspace_busy" | "storage" | "server"
|
|
4
|
+
/** `401 invalid_user_hash`: the workspace requires a signed identity and `userHash` is wrong or missing. */
|
|
5
|
+
| "identity_rejected"
|
|
6
|
+
/** `429 rate_limited`: wait `retryAfterSeconds` before trying again. */
|
|
7
|
+
| "rate_limited";
|
|
8
|
+
|
|
9
|
+
export class GiovChatError extends Error {
|
|
10
|
+
constructor(
|
|
11
|
+
public readonly code: GiovChatErrorCode,
|
|
12
|
+
message: string,
|
|
13
|
+
public readonly status?: number,
|
|
14
|
+
/** From `Retry-After` on `429` and `503` answers, in seconds. */
|
|
15
|
+
public readonly retryAfterSeconds?: number,
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "GiovChatError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function chatError(error: unknown): GiovChatError {
|
|
23
|
+
return error instanceof GiovChatError ? error : new GiovChatError("network", "Unable to reach chat. Please try again.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `Retry-After` in seconds (the protocol never sends a date); undefined when absent or malformed. */
|
|
27
|
+
export function retryAfterSeconds(value: string | null | undefined): number | undefined {
|
|
28
|
+
const seconds = Number(value?.trim());
|
|
29
|
+
return value?.trim() && Number.isFinite(seconds) && seconds >= 0 ? Math.min(seconds, 3600) : undefined;
|
|
30
|
+
}
|
package/src/i18n.ts
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's own interface text in the five languages the Giov dashboard ships. Text the business
|
|
5
|
+
* writes (greeting, placeholder, bot name) comes from the widget configuration and is not translated.
|
|
6
|
+
* Hosts pick the language with the `locale` prop (default: the device language) and can replace any
|
|
7
|
+
* string with `strings`.
|
|
8
|
+
*/
|
|
9
|
+
export interface GiovChatStrings {
|
|
10
|
+
loading: string;
|
|
11
|
+
tryAgain: string;
|
|
12
|
+
retry: string;
|
|
13
|
+
closeChat: string;
|
|
14
|
+
/** The header menu button. */
|
|
15
|
+
menu: string;
|
|
16
|
+
/** The header menu's link to the workspace's privacy policy, as in the web widget. */
|
|
17
|
+
privacyPolicy: string;
|
|
18
|
+
/** Spoken when a reply arrives: who wrote it and what they wrote. */
|
|
19
|
+
newMessage: (sender: string, text: string) => string;
|
|
20
|
+
connecting: string;
|
|
21
|
+
reconnecting: string;
|
|
22
|
+
statusPerson: string;
|
|
23
|
+
statusBot: string;
|
|
24
|
+
statusTeam: string;
|
|
25
|
+
composerPlaceholder: string;
|
|
26
|
+
messageInput: string;
|
|
27
|
+
sendMessage: string;
|
|
28
|
+
attachFile: string;
|
|
29
|
+
removeAttachment: (name: string) => string;
|
|
30
|
+
you: string;
|
|
31
|
+
agent: string;
|
|
32
|
+
automated: string;
|
|
33
|
+
botTyping: (name: string) => string;
|
|
34
|
+
teamTyping: string;
|
|
35
|
+
sending: string;
|
|
36
|
+
notSent: string;
|
|
37
|
+
tapToRetry: string;
|
|
38
|
+
/** On a message that was not sent, to drop it. */
|
|
39
|
+
discard: string;
|
|
40
|
+
uploading: (percent: number) => string;
|
|
41
|
+
uploadFailed: string;
|
|
42
|
+
/** Errors, by kind; the SDK never shows raw server text. */
|
|
43
|
+
sessionExpired: string;
|
|
44
|
+
conversationEnded: string;
|
|
45
|
+
unavailable: string;
|
|
46
|
+
sendFailed: string;
|
|
47
|
+
busy: string;
|
|
48
|
+
attachmentsOff: string;
|
|
49
|
+
typeNotAllowed: (name: string) => string;
|
|
50
|
+
fileTooLarge: (name: string, maxMb: number) => string;
|
|
51
|
+
tooManyFiles: (max: number) => string;
|
|
52
|
+
openAttachment: (name: string) => string;
|
|
53
|
+
attachment: string;
|
|
54
|
+
image: string;
|
|
55
|
+
/** In place of an image that could not load (it loads again once its link is renewed). */
|
|
56
|
+
imageUnavailable: string;
|
|
57
|
+
preChatTitle: string;
|
|
58
|
+
firstName: string;
|
|
59
|
+
lastName: string;
|
|
60
|
+
email: string;
|
|
61
|
+
phone: string;
|
|
62
|
+
required: (label: string) => string;
|
|
63
|
+
startChat: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type GiovChatLocale = "en" | "fr" | "es" | "pt" | "de";
|
|
67
|
+
|
|
68
|
+
const en: GiovChatStrings = {
|
|
69
|
+
loading: "Loading…",
|
|
70
|
+
tryAgain: "Try again",
|
|
71
|
+
retry: "Retry",
|
|
72
|
+
closeChat: "Close chat",
|
|
73
|
+
menu: "Menu",
|
|
74
|
+
privacyPolicy: "GDPR and privacy",
|
|
75
|
+
newMessage: (sender, text) => `${sender}: ${text}`,
|
|
76
|
+
connecting: "Connecting…",
|
|
77
|
+
reconnecting: "Connection lost. Reconnecting…",
|
|
78
|
+
statusPerson: "Live agent",
|
|
79
|
+
statusBot: "AI assistant · Online",
|
|
80
|
+
statusTeam: "We'll be with you shortly",
|
|
81
|
+
composerPlaceholder: "Type a message…",
|
|
82
|
+
messageInput: "Message",
|
|
83
|
+
sendMessage: "Send message",
|
|
84
|
+
attachFile: "Attach file",
|
|
85
|
+
removeAttachment: (name) => `Remove ${name}`,
|
|
86
|
+
you: "You",
|
|
87
|
+
agent: "Agent",
|
|
88
|
+
automated: "Automated",
|
|
89
|
+
botTyping: (name) => `${name} is typing`,
|
|
90
|
+
teamTyping: "Support team is typing",
|
|
91
|
+
sending: "Sending…",
|
|
92
|
+
notSent: "Not sent",
|
|
93
|
+
tapToRetry: "Tap to retry",
|
|
94
|
+
discard: "Discard",
|
|
95
|
+
uploading: (percent) => `Uploading ${percent}%`,
|
|
96
|
+
uploadFailed: "File upload failed",
|
|
97
|
+
sessionExpired: "Chat session expired. Please start a new chat.",
|
|
98
|
+
conversationEnded: "This conversation has ended.",
|
|
99
|
+
unavailable: "Chat is unavailable. Please try again.",
|
|
100
|
+
sendFailed: "Message could not be sent.",
|
|
101
|
+
busy: "The team is busy right now. Please try again in a moment.",
|
|
102
|
+
attachmentsOff: "File attachments are turned off for this chat.",
|
|
103
|
+
typeNotAllowed: (name) => `${name}: this file type is not allowed.`,
|
|
104
|
+
fileTooLarge: (name, maxMb) => `${name}: files must be ${maxMb} MB or smaller.`,
|
|
105
|
+
tooManyFiles: (max) => `You can attach up to ${max} files at a time.`,
|
|
106
|
+
openAttachment: (name) => `Open ${name}`,
|
|
107
|
+
attachment: "Attachment",
|
|
108
|
+
image: "Image",
|
|
109
|
+
imageUnavailable: "Image unavailable",
|
|
110
|
+
preChatTitle: "Before we start…",
|
|
111
|
+
firstName: "First name",
|
|
112
|
+
lastName: "Last name",
|
|
113
|
+
email: "Email",
|
|
114
|
+
phone: "Phone",
|
|
115
|
+
required: (label) => `${label} *`,
|
|
116
|
+
startChat: "Start chat",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const fr: GiovChatStrings = {
|
|
120
|
+
loading: "Chargement…",
|
|
121
|
+
tryAgain: "Réessayer",
|
|
122
|
+
retry: "Réessayer",
|
|
123
|
+
closeChat: "Fermer le chat",
|
|
124
|
+
menu: "Menu",
|
|
125
|
+
privacyPolicy: "RGPD et confidentialité",
|
|
126
|
+
newMessage: (sender, text) => `${sender} : ${text}`,
|
|
127
|
+
connecting: "Connexion…",
|
|
128
|
+
reconnecting: "Connexion perdue. Reconnexion…",
|
|
129
|
+
statusPerson: "Conseiller en ligne",
|
|
130
|
+
statusBot: "Assistant IA · En ligne",
|
|
131
|
+
statusTeam: "Nous vous répondons très vite",
|
|
132
|
+
composerPlaceholder: "Écrivez un message…",
|
|
133
|
+
messageInput: "Message",
|
|
134
|
+
sendMessage: "Envoyer le message",
|
|
135
|
+
attachFile: "Joindre un fichier",
|
|
136
|
+
removeAttachment: (name) => `Retirer ${name}`,
|
|
137
|
+
you: "Vous",
|
|
138
|
+
agent: "Conseiller",
|
|
139
|
+
automated: "Automatique",
|
|
140
|
+
botTyping: (name) => `${name} écrit`,
|
|
141
|
+
teamTyping: "L'équipe écrit",
|
|
142
|
+
sending: "Envoi…",
|
|
143
|
+
notSent: "Non envoyé",
|
|
144
|
+
tapToRetry: "Touchez pour réessayer",
|
|
145
|
+
discard: "Supprimer",
|
|
146
|
+
uploading: (percent) => `Envoi ${percent} %`,
|
|
147
|
+
uploadFailed: "Échec de l'envoi du fichier",
|
|
148
|
+
sessionExpired: "La session de chat a expiré. Démarrez un nouveau chat.",
|
|
149
|
+
conversationEnded: "Cette conversation est terminée.",
|
|
150
|
+
unavailable: "Le chat est indisponible. Réessayez.",
|
|
151
|
+
sendFailed: "Le message n'a pas pu être envoyé.",
|
|
152
|
+
busy: "L'équipe est très occupée. Réessayez dans un instant.",
|
|
153
|
+
attachmentsOff: "Les pièces jointes sont désactivées pour ce chat.",
|
|
154
|
+
typeNotAllowed: (name) => `${name} : ce type de fichier n'est pas autorisé.`,
|
|
155
|
+
fileTooLarge: (name, maxMb) => `${name} : les fichiers doivent faire ${maxMb} Mo maximum.`,
|
|
156
|
+
tooManyFiles: (max) => `Vous pouvez joindre jusqu'à ${max} fichiers à la fois.`,
|
|
157
|
+
openAttachment: (name) => `Ouvrir ${name}`,
|
|
158
|
+
attachment: "Pièce jointe",
|
|
159
|
+
image: "Image",
|
|
160
|
+
imageUnavailable: "Image indisponible",
|
|
161
|
+
preChatTitle: "Avant de commencer…",
|
|
162
|
+
firstName: "Prénom",
|
|
163
|
+
lastName: "Nom",
|
|
164
|
+
email: "E-mail",
|
|
165
|
+
phone: "Téléphone",
|
|
166
|
+
required: (label) => `${label} *`,
|
|
167
|
+
startChat: "Démarrer le chat",
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const es: GiovChatStrings = {
|
|
171
|
+
loading: "Cargando…",
|
|
172
|
+
tryAgain: "Reintentar",
|
|
173
|
+
retry: "Reintentar",
|
|
174
|
+
closeChat: "Cerrar chat",
|
|
175
|
+
menu: "Menú",
|
|
176
|
+
privacyPolicy: "RGPD y privacidad",
|
|
177
|
+
newMessage: (sender, text) => `${sender}: ${text}`,
|
|
178
|
+
connecting: "Conectando…",
|
|
179
|
+
reconnecting: "Conexión perdida. Reconectando…",
|
|
180
|
+
statusPerson: "Agente en vivo",
|
|
181
|
+
statusBot: "Asistente de IA · En línea",
|
|
182
|
+
statusTeam: "Te atenderemos en breve",
|
|
183
|
+
composerPlaceholder: "Escribe un mensaje…",
|
|
184
|
+
messageInput: "Mensaje",
|
|
185
|
+
sendMessage: "Enviar mensaje",
|
|
186
|
+
attachFile: "Adjuntar archivo",
|
|
187
|
+
removeAttachment: (name) => `Quitar ${name}`,
|
|
188
|
+
you: "Tú",
|
|
189
|
+
agent: "Agente",
|
|
190
|
+
automated: "Automático",
|
|
191
|
+
botTyping: (name) => `${name} está escribiendo`,
|
|
192
|
+
teamTyping: "El equipo está escribiendo",
|
|
193
|
+
sending: "Enviando…",
|
|
194
|
+
notSent: "No enviado",
|
|
195
|
+
tapToRetry: "Toca para reintentar",
|
|
196
|
+
discard: "Descartar",
|
|
197
|
+
uploading: (percent) => `Subiendo ${percent} %`,
|
|
198
|
+
uploadFailed: "No se pudo subir el archivo",
|
|
199
|
+
sessionExpired: "La sesión de chat caducó. Inicia un chat nuevo.",
|
|
200
|
+
conversationEnded: "Esta conversación ha terminado.",
|
|
201
|
+
unavailable: "El chat no está disponible. Inténtalo de nuevo.",
|
|
202
|
+
sendFailed: "No se pudo enviar el mensaje.",
|
|
203
|
+
busy: "El equipo está ocupado ahora. Inténtalo de nuevo en un momento.",
|
|
204
|
+
attachmentsOff: "Los archivos adjuntos están desactivados en este chat.",
|
|
205
|
+
typeNotAllowed: (name) => `${name}: este tipo de archivo no está permitido.`,
|
|
206
|
+
fileTooLarge: (name, maxMb) => `${name}: los archivos deben pesar ${maxMb} MB o menos.`,
|
|
207
|
+
tooManyFiles: (max) => `Puedes adjuntar hasta ${max} archivos a la vez.`,
|
|
208
|
+
openAttachment: (name) => `Abrir ${name}`,
|
|
209
|
+
attachment: "Archivo adjunto",
|
|
210
|
+
image: "Imagen",
|
|
211
|
+
imageUnavailable: "Imagen no disponible",
|
|
212
|
+
preChatTitle: "Antes de empezar…",
|
|
213
|
+
firstName: "Nombre",
|
|
214
|
+
lastName: "Apellido",
|
|
215
|
+
email: "Correo electrónico",
|
|
216
|
+
phone: "Teléfono",
|
|
217
|
+
required: (label) => `${label} *`,
|
|
218
|
+
startChat: "Iniciar chat",
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const pt: GiovChatStrings = {
|
|
222
|
+
loading: "Carregando…",
|
|
223
|
+
tryAgain: "Tentar novamente",
|
|
224
|
+
retry: "Tentar novamente",
|
|
225
|
+
closeChat: "Fechar chat",
|
|
226
|
+
menu: "Menu",
|
|
227
|
+
privacyPolicy: "RGPD e privacidade",
|
|
228
|
+
newMessage: (sender, text) => `${sender}: ${text}`,
|
|
229
|
+
connecting: "Conectando…",
|
|
230
|
+
reconnecting: "Conexão perdida. Reconectando…",
|
|
231
|
+
statusPerson: "Atendente ao vivo",
|
|
232
|
+
statusBot: "Assistente de IA · Online",
|
|
233
|
+
statusTeam: "Responderemos em breve",
|
|
234
|
+
composerPlaceholder: "Digite uma mensagem…",
|
|
235
|
+
messageInput: "Mensagem",
|
|
236
|
+
sendMessage: "Enviar mensagem",
|
|
237
|
+
attachFile: "Anexar arquivo",
|
|
238
|
+
removeAttachment: (name) => `Remover ${name}`,
|
|
239
|
+
you: "Você",
|
|
240
|
+
agent: "Atendente",
|
|
241
|
+
automated: "Automático",
|
|
242
|
+
botTyping: (name) => `${name} está digitando`,
|
|
243
|
+
teamTyping: "A equipe está digitando",
|
|
244
|
+
sending: "Enviando…",
|
|
245
|
+
notSent: "Não enviada",
|
|
246
|
+
tapToRetry: "Toque para tentar novamente",
|
|
247
|
+
discard: "Descartar",
|
|
248
|
+
uploading: (percent) => `Enviando ${percent}%`,
|
|
249
|
+
uploadFailed: "Falha ao enviar o arquivo",
|
|
250
|
+
sessionExpired: "A sessão do chat expirou. Inicie um novo chat.",
|
|
251
|
+
conversationEnded: "Esta conversa foi encerrada.",
|
|
252
|
+
unavailable: "O chat está indisponível. Tente novamente.",
|
|
253
|
+
sendFailed: "Não foi possível enviar a mensagem.",
|
|
254
|
+
busy: "A equipe está ocupada agora. Tente novamente em instantes.",
|
|
255
|
+
attachmentsOff: "Os anexos estão desativados neste chat.",
|
|
256
|
+
typeNotAllowed: (name) => `${name}: este tipo de arquivo não é permitido.`,
|
|
257
|
+
fileTooLarge: (name, maxMb) => `${name}: os arquivos devem ter até ${maxMb} MB.`,
|
|
258
|
+
tooManyFiles: (max) => `Você pode anexar até ${max} arquivos por vez.`,
|
|
259
|
+
openAttachment: (name) => `Abrir ${name}`,
|
|
260
|
+
attachment: "Anexo",
|
|
261
|
+
image: "Imagem",
|
|
262
|
+
imageUnavailable: "Imagem indisponível",
|
|
263
|
+
preChatTitle: "Antes de começar…",
|
|
264
|
+
firstName: "Nome",
|
|
265
|
+
lastName: "Sobrenome",
|
|
266
|
+
email: "E-mail",
|
|
267
|
+
phone: "Telefone",
|
|
268
|
+
required: (label) => `${label} *`,
|
|
269
|
+
startChat: "Iniciar chat",
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const de: GiovChatStrings = {
|
|
273
|
+
loading: "Wird geladen…",
|
|
274
|
+
tryAgain: "Erneut versuchen",
|
|
275
|
+
retry: "Erneut versuchen",
|
|
276
|
+
closeChat: "Chat schließen",
|
|
277
|
+
menu: "Menü",
|
|
278
|
+
privacyPolicy: "DSGVO und Datenschutz",
|
|
279
|
+
newMessage: (sender, text) => `${sender}: ${text}`,
|
|
280
|
+
connecting: "Verbindung wird hergestellt…",
|
|
281
|
+
reconnecting: "Verbindung unterbrochen. Neuer Versuch…",
|
|
282
|
+
statusPerson: "Live-Support",
|
|
283
|
+
statusBot: "KI-Assistent · Online",
|
|
284
|
+
statusTeam: "Wir sind gleich für dich da",
|
|
285
|
+
composerPlaceholder: "Nachricht schreiben…",
|
|
286
|
+
messageInput: "Nachricht",
|
|
287
|
+
sendMessage: "Nachricht senden",
|
|
288
|
+
attachFile: "Datei anhängen",
|
|
289
|
+
removeAttachment: (name) => `${name} entfernen`,
|
|
290
|
+
you: "Du",
|
|
291
|
+
agent: "Support",
|
|
292
|
+
automated: "Automatisch",
|
|
293
|
+
botTyping: (name) => `${name} schreibt`,
|
|
294
|
+
teamTyping: "Das Team schreibt",
|
|
295
|
+
sending: "Wird gesendet…",
|
|
296
|
+
notSent: "Nicht gesendet",
|
|
297
|
+
tapToRetry: "Tippen zum Wiederholen",
|
|
298
|
+
discard: "Verwerfen",
|
|
299
|
+
uploading: (percent) => `Wird hochgeladen: ${percent} %`,
|
|
300
|
+
uploadFailed: "Datei konnte nicht hochgeladen werden",
|
|
301
|
+
sessionExpired: "Die Chat-Sitzung ist abgelaufen. Starte einen neuen Chat.",
|
|
302
|
+
conversationEnded: "Diese Unterhaltung ist beendet.",
|
|
303
|
+
unavailable: "Der Chat ist nicht verfügbar. Bitte versuche es erneut.",
|
|
304
|
+
sendFailed: "Die Nachricht konnte nicht gesendet werden.",
|
|
305
|
+
busy: "Das Team ist gerade ausgelastet. Bitte versuche es gleich noch einmal.",
|
|
306
|
+
attachmentsOff: "Dateianhänge sind in diesem Chat deaktiviert.",
|
|
307
|
+
typeNotAllowed: (name) => `${name}: Dieser Dateityp ist nicht erlaubt.`,
|
|
308
|
+
fileTooLarge: (name, maxMb) => `${name}: Dateien dürfen höchstens ${maxMb} MB groß sein.`,
|
|
309
|
+
tooManyFiles: (max) => `Du kannst bis zu ${max} Dateien auf einmal anhängen.`,
|
|
310
|
+
openAttachment: (name) => `${name} öffnen`,
|
|
311
|
+
attachment: "Anhang",
|
|
312
|
+
image: "Bild",
|
|
313
|
+
imageUnavailable: "Bild nicht verfügbar",
|
|
314
|
+
preChatTitle: "Bevor es losgeht…",
|
|
315
|
+
firstName: "Vorname",
|
|
316
|
+
lastName: "Nachname",
|
|
317
|
+
email: "E-Mail",
|
|
318
|
+
phone: "Telefon",
|
|
319
|
+
required: (label) => `${label} *`,
|
|
320
|
+
startChat: "Chat starten",
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export const GIOVCHAT_STRINGS: Readonly<Record<GiovChatLocale, GiovChatStrings>> = { en, fr, es, pt, de };
|
|
324
|
+
|
|
325
|
+
/** "fr-CA" → "fr", "pt_BR" → "pt"; anything not shipped falls back to English. */
|
|
326
|
+
export function resolveLocale(value: string | null | undefined): GiovChatLocale {
|
|
327
|
+
const language = String(value ?? "").trim().toLowerCase().split(/[-_]/)[0];
|
|
328
|
+
return (Object.keys(GIOVCHAT_STRINGS) as GiovChatLocale[]).includes(language as GiovChatLocale) ? language as GiovChatLocale : "en";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The device language from Intl (Hermes and JSC both ship it), with no native module. */
|
|
332
|
+
export function deviceLocale(): string {
|
|
333
|
+
try { return Intl.DateTimeFormat().resolvedOptions().locale || "en"; } catch { return "en"; }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** The strings for a locale with the host's overrides on top; malformed overrides are ignored. */
|
|
337
|
+
export function resolveStrings(locale?: string | null, overrides?: Partial<GiovChatStrings> | null): GiovChatStrings {
|
|
338
|
+
const base = GIOVCHAT_STRINGS[resolveLocale(locale ?? deviceLocale())];
|
|
339
|
+
if (!overrides || typeof overrides !== "object") return base;
|
|
340
|
+
const result = { ...base } as Record<string, unknown>;
|
|
341
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
342
|
+
const current = (base as unknown as Record<string, unknown>)[key];
|
|
343
|
+
if (current === undefined || typeof value !== typeof current) continue;
|
|
344
|
+
if (typeof value === "string" && !value.trim()) continue;
|
|
345
|
+
result[key] = value;
|
|
346
|
+
}
|
|
347
|
+
return result as unknown as GiovChatStrings;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export const GiovChatStringsContext = createContext<GiovChatStrings>(en);
|
|
351
|
+
export const useGiovChatStrings = () => useContext(GiovChatStringsContext);
|