@zenify-omni/chat-js 0.1.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.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -0
  3. package/api/campaign.js +19 -0
  4. package/api/chat.js +518 -0
  5. package/api/chatroom.js +254 -0
  6. package/api/contact.js +45 -0
  7. package/api/crm-organization.js +101 -0
  8. package/api/customer.js +143 -0
  9. package/api/facebook-labels.js +11 -0
  10. package/api/field.js +11 -0
  11. package/api/file.js +17 -0
  12. package/api/filter.js +50 -0
  13. package/api/group.js +11 -0
  14. package/api/iam.js +23 -0
  15. package/api/index.js +54 -0
  16. package/api/orgTree.js +45 -0
  17. package/api/post.js +111 -0
  18. package/api/queue.js +15 -0
  19. package/api/report.js +422 -0
  20. package/api/send.js +21 -0
  21. package/api/session-event.js +30 -0
  22. package/api/session.js +249 -0
  23. package/api/settings/assign-task.js +10 -0
  24. package/api/settings/channel.js +136 -0
  25. package/api/settings/chat-layout.js +37 -0
  26. package/api/settings/chat-timeout.js +10 -0
  27. package/api/settings/common.js +32 -0
  28. package/api/settings/config.js +19 -0
  29. package/api/settings/index.js +24 -0
  30. package/api/settings/ivr.js +25 -0
  31. package/api/settings/queue-v3.js +56 -0
  32. package/api/settings/queue.js +48 -0
  33. package/api/settings/routing.js +32 -0
  34. package/api/settings/service.js +31 -0
  35. package/api/settings.js +567 -0
  36. package/api/shipping.js +19 -0
  37. package/api/shopee.js +21 -0
  38. package/api/smart-qc/index.js +4 -0
  39. package/api/smart-qc/keyword.js +10 -0
  40. package/api/tag.js +11 -0
  41. package/api/user.js +177 -0
  42. package/client.js +391 -0
  43. package/config/env.js +76 -0
  44. package/constants/advance-filter.js +358 -0
  45. package/constants/auto-assign-tag.js +13 -0
  46. package/constants/chat-message.js +423 -0
  47. package/constants/chat-room-tag.js +7 -0
  48. package/constants/chat-source.js +68 -0
  49. package/constants/chatbot.js +58 -0
  50. package/constants/common.js +4 -0
  51. package/constants/crm-organization.js +75 -0
  52. package/constants/distribute.js +8 -0
  53. package/constants/distribution-log.js +14 -0
  54. package/constants/events.js +27 -0
  55. package/constants/file-upload.js +86 -0
  56. package/constants/lead.js +111 -0
  57. package/constants/mask-data-config.js +16 -0
  58. package/constants/month.js +78 -0
  59. package/constants/quick-filter.js +49 -0
  60. package/constants/regex.js +34 -0
  61. package/constants/setting.js +62 -0
  62. package/constants/shipping.js +12 -0
  63. package/constants/social.js +21 -0
  64. package/constants/sortable-fields.js +146 -0
  65. package/constants/tenant_config.js +154 -0
  66. package/constants/use-query-key.js +470 -0
  67. package/constants/user.js +293 -0
  68. package/exceptions/axios-error.js +24 -0
  69. package/exceptions/https-error.js +28 -0
  70. package/exceptions/validator-error.js +20 -0
  71. package/lib/avatar-color.js +31 -0
  72. package/lib/i18n/index.js +85 -0
  73. package/lib/i18n/language-store.js +74 -0
  74. package/lib/i18n/locales/en/campaign.json +3 -0
  75. package/lib/i18n/locales/en/chat.json +656 -0
  76. package/lib/i18n/locales/en/common.json +1846 -0
  77. package/lib/i18n/locales/en/customer.json +20 -0
  78. package/lib/i18n/locales/en/lead.json +5 -0
  79. package/lib/i18n/locales/en/lumi.json +4 -0
  80. package/lib/i18n/locales/en/notification.json +107 -0
  81. package/lib/i18n/locales/en/org.json +4 -0
  82. package/lib/i18n/locales/en/posts.json +8 -0
  83. package/lib/i18n/locales/en/settings.json +39 -0
  84. package/lib/i18n/locales/vi/campaign.json +3 -0
  85. package/lib/i18n/locales/vi/chat.json +660 -0
  86. package/lib/i18n/locales/vi/common.json +1846 -0
  87. package/lib/i18n/locales/vi/customer.json +20 -0
  88. package/lib/i18n/locales/vi/lead.json +5 -0
  89. package/lib/i18n/locales/vi/lumi.json +4 -0
  90. package/lib/i18n/locales/vi/notification.json +107 -0
  91. package/lib/i18n/locales/vi/org.json +4 -0
  92. package/lib/i18n/locales/vi/posts.json +8 -0
  93. package/lib/i18n/locales/vi/settings.json +39 -0
  94. package/lib/i18n/server-label.js +33 -0
  95. package/lib/phone.js +70 -0
  96. package/lib/user-avatar.js +21 -0
  97. package/package.json +45 -0
  98. package/socket/index.js +30 -0
  99. package/transport-registry.js +77 -0
  100. package/utils/access-detail.js +119 -0
  101. package/utils/auth.js +205 -0
  102. package/utils/chunk-reload-guard.js +4 -0
  103. package/utils/concurrency-limiter.js +30 -0
  104. package/utils/crm-organization.js +0 -0
  105. package/utils/currency.js +14 -0
  106. package/utils/customers.js +42 -0
  107. package/utils/data.js +178 -0
  108. package/utils/date.js +144 -0
  109. package/utils/debounced-invalidate.js +15 -0
  110. package/utils/emoticon.js +134 -0
  111. package/utils/export.js +7 -0
  112. package/utils/field-layout.js +33 -0
  113. package/utils/filter-adapters.js +9 -0
  114. package/utils/filter.js +18 -0
  115. package/utils/formatters.js +6 -0
  116. package/utils/image-compression.js +169 -0
  117. package/utils/leads.js +13 -0
  118. package/utils/microlink.js +20 -0
  119. package/utils/org-tree.js +108 -0
  120. package/utils/request.js +461 -0
  121. package/utils/session-end.js +19 -0
  122. package/utils/string.js +154 -0
  123. package/utils/table-display-config.js +80 -0
  124. package/utils/validation.js +49 -0
@@ -0,0 +1,293 @@
1
+ import i18n from "../lib/i18n/index.js";
2
+ const UserRole = Object.freeze({
3
+ // customer
4
+ END_USER: "end_user",
5
+ // user
6
+ AGENT: "agent",
7
+ TEAM_LEADER: "team_leader",
8
+ SUPERVISOR: "supervisor",
9
+ ADMINISTRATOR: "administrator",
10
+ // sale
11
+ SALE: "sale",
12
+ SALE_TEAM_LEADER: "sale_team_leader",
13
+ // Chatbot
14
+ CHATBOT: "chatbot"
15
+ });
16
+ const UserRoleContext = Object.freeze({
17
+ get [UserRole.END_USER]() {
18
+ return i18n.t("common:label.customer", {
19
+ defaultValue: "Kh\xE1ch h\xE0ng"
20
+ });
21
+ },
22
+ get [UserRole.AGENT]() {
23
+ return i18n.t("common:label.agent", {
24
+ defaultValue: "Chuy\xEAn vi\xEAn"
25
+ });
26
+ },
27
+ get [UserRole.TEAM_LEADER]() {
28
+ return i18n.t("common:label.departmentHead", {
29
+ defaultValue: "Tr\u01B0\u1EDFng b\u1ED9 ph\u1EADn"
30
+ });
31
+ },
32
+ [UserRole.SUPERVISOR]: "Supervisor",
33
+ [UserRole.ADMINISTRATOR]: "Administrator",
34
+ // sale
35
+ [UserRole.SALE]: "Sale",
36
+ get [UserRole.SALE_TEAM_LEADER]() {
37
+ return i18n.t("common:label.salesDepartmentHead", {
38
+ defaultValue: "Tr\u01B0\u1EDFng b\u1ED9 ph\u1EADn Sale"
39
+ });
40
+ },
41
+ // Chatbot
42
+ [UserRole.CHATBOT]: "AI Agent"
43
+ });
44
+ const AssigneeRoles = Object.freeze([
45
+ UserRole.ADMINISTRATOR,
46
+ UserRole.AGENT,
47
+ UserRole.SUPERVISOR,
48
+ UserRole.TEAM_LEADER
49
+ ]);
50
+ const SeniorStaffRoles = Object.freeze([
51
+ UserRole.ADMINISTRATOR,
52
+ UserRole.SUPERVISOR,
53
+ UserRole.TEAM_LEADER,
54
+ UserRole.SALE_TEAM_LEADER
55
+ ]);
56
+ const CustomerType = Object.freeze({
57
+ ECONOMY: "0",
58
+ SILVER: "1",
59
+ GOLD: "2",
60
+ DIAMOND: "3"
61
+ });
62
+ const CustomerTypeContext = Object.freeze({
63
+ get [CustomerType.ECONOMY]() {
64
+ return i18n.t("common:label.rankRegular", {
65
+ defaultValue: "Th\u01B0\u1EDDng"
66
+ });
67
+ },
68
+ get [CustomerType.SILVER]() {
69
+ return i18n.t("common:label.silver", { defaultValue: "B\u1EA1c" });
70
+ },
71
+ get [CustomerType.GOLD]() {
72
+ return i18n.t("common:label.rankGold", {
73
+ defaultValue: "V\xE0ng"
74
+ });
75
+ },
76
+ get [CustomerType.DIAMOND]() {
77
+ return i18n.t("common:label.rankDiamond", {
78
+ defaultValue: "Kim c\u01B0\u01A1ng"
79
+ });
80
+ }
81
+ });
82
+ const SourceUser = Object.freeze({
83
+ WEB: "web",
84
+ CALL_IN: "CALL_INBOUND",
85
+ CALL_OUT: "CALL_OUTBOUND",
86
+ CALL: "call",
87
+ CHAT: "chat",
88
+ ZALO: "zalo",
89
+ FACEBOOK: "facebook",
90
+ EMAIL: "email",
91
+ HAND: "hand",
92
+ API: "api",
93
+ IMPORT: "import",
94
+ WHATSAPP: "whatsapp",
95
+ TIKTOK: "tiktok",
96
+ TIKTOK_SHOP: "tiktok_shop",
97
+ INSTAGRAM: "instagram",
98
+ SHOPEE: "shopee"
99
+ });
100
+ const SourceUserContext = Object.freeze({
101
+ [SourceUser.WEB]: "Livechat",
102
+ get [SourceUser.CALL_OUT]() {
103
+ return i18n.t("common:label.callOut", {
104
+ defaultValue: "G\u1ECDi ra"
105
+ });
106
+ },
107
+ get [SourceUser.CALL_IN]() {
108
+ return i18n.t("common:label.callIn", {
109
+ defaultValue: "G\u1ECDi v\xE0o"
110
+ });
111
+ },
112
+ [SourceUser.CHAT]: "Livechat",
113
+ [SourceUser.ZALO]: "Zalo",
114
+ [SourceUser.FACEBOOK]: "Facebook",
115
+ [SourceUser.EMAIL]: "Email",
116
+ get [SourceUser.HAND]() {
117
+ return i18n.t("common:label.manual", {
118
+ defaultValue: "Th\u1EE7 c\xF4ng"
119
+ });
120
+ },
121
+ [SourceUser.API]: "Api",
122
+ [SourceUser.IMPORT]: "Import",
123
+ [SourceUser.WHATSAPP]: "WhatsApp",
124
+ [SourceUser.TIKTOK]: "Tiktok Business",
125
+ [SourceUser.TIKTOK_SHOP]: "Tiktok Shop",
126
+ [SourceUser.INSTAGRAM]: "Instagram",
127
+ [SourceUser.SHOPEE]: "Shopee"
128
+ });
129
+ const CustomerAssign = Object.freeze({
130
+ ALL: "all",
131
+ MyOrganization: "my_organization",
132
+ MyGroup: "my_group",
133
+ MyCustomer: "me"
134
+ });
135
+ const Gender = Object.freeze({
136
+ MALE: "male",
137
+ FEMALE: "female",
138
+ OTHER: "other"
139
+ });
140
+ const GenderContext = Object.freeze({
141
+ [Gender.MALE]: "Nam",
142
+ get [Gender.FEMALE]() {
143
+ return i18n.t("common:label.female", { defaultValue: "N\u1EEF" });
144
+ },
145
+ get [Gender.OTHER]() {
146
+ return i18n.t("common:label.other", { defaultValue: "Kh\xE1c" });
147
+ }
148
+ });
149
+ const GenderOptions = [
150
+ { id: Gender.MALE, text: GenderContext[Gender.MALE] },
151
+ { id: Gender.FEMALE, text: GenderContext[Gender.FEMALE] },
152
+ { id: Gender.OTHER, text: GenderContext[Gender.OTHER] }
153
+ ];
154
+ const SourceUserOptions = [
155
+ {
156
+ text: SourceUserContext[SourceUser.HAND],
157
+ id: SourceUser.HAND
158
+ },
159
+ {
160
+ text: SourceUserContext[SourceUser.WEB],
161
+ id: SourceUser.WEB
162
+ },
163
+ {
164
+ text: SourceUserContext[SourceUser.CALL_OUT],
165
+ id: SourceUser.CALL_OUT
166
+ },
167
+ {
168
+ text: SourceUserContext[SourceUser.CALL_IN],
169
+ id: SourceUser.CALL_IN
170
+ },
171
+ {
172
+ text: SourceUserContext[SourceUser.ZALO],
173
+ id: SourceUser.ZALO
174
+ },
175
+ {
176
+ text: SourceUserContext[SourceUser.FACEBOOK],
177
+ id: SourceUser.FACEBOOK
178
+ },
179
+ {
180
+ text: SourceUserContext[SourceUser.EMAIL],
181
+ id: SourceUser.EMAIL
182
+ },
183
+ {
184
+ text: SourceUserContext[SourceUser.WHATSAPP],
185
+ id: SourceUser.WHATSAPP
186
+ },
187
+ {
188
+ text: SourceUserContext[SourceUser.API],
189
+ id: SourceUser.API
190
+ },
191
+ {
192
+ text: SourceUserContext[SourceUser.IMPORT],
193
+ id: SourceUser.IMPORT
194
+ },
195
+ {
196
+ text: SourceUserContext[SourceUser.TIKTOK],
197
+ id: SourceUser.TIKTOK
198
+ },
199
+ {
200
+ text: SourceUserContext[SourceUser.INSTAGRAM],
201
+ id: SourceUser.INSTAGRAM
202
+ },
203
+ {
204
+ text: SourceUserContext[SourceUser.SHOPEE],
205
+ id: SourceUser.SHOPEE
206
+ }
207
+ ];
208
+ const CrmType = Object.freeze({
209
+ Contact: "contact",
210
+ Lead: "lead",
211
+ Internal: "internal"
212
+ // used by internal call only
213
+ });
214
+ const DuplicateContactWarningType = Object.freeze({
215
+ Phone: "DUPLICATE_PHONE",
216
+ CustomerCode: "DUPLICATE_CUSTOMER_CODE"
217
+ });
218
+ const isDuplicateContactWarning = (type) => type === DuplicateContactWarningType.Phone || type === DuplicateContactWarningType.CustomerCode;
219
+ const ConversionSource = Object.freeze({
220
+ IMPORT_FILE: "import_file",
221
+ FROM_LEAD: "from_lead",
222
+ MANUAL: "manual",
223
+ WORKFLOW: "workflow",
224
+ API: "api",
225
+ SYSTEM: "system"
226
+ });
227
+ const ConversionSourceContext = Object.freeze({
228
+ [ConversionSource.IMPORT_FILE]: "Import file",
229
+ get [ConversionSource.FROM_LEAD]() {
230
+ return i18n.t("common:label.convertedFromLead", {
231
+ defaultValue: "Chuy\u1EC3n t\u1EEB ti\u1EC1m n\u0103ng"
232
+ });
233
+ },
234
+ get [ConversionSource.MANUAL]() {
235
+ return i18n.t("common:label.manual", {
236
+ defaultValue: "Th\u1EE7 c\xF4ng"
237
+ });
238
+ },
239
+ [ConversionSource.WORKFLOW]: "Workflow",
240
+ [ConversionSource.API]: "API",
241
+ get [ConversionSource.SYSTEM]() {
242
+ return i18n.t("common:label.system", {
243
+ defaultValue: "H\u1EC7 th\u1ED1ng"
244
+ });
245
+ }
246
+ });
247
+ const ConversionSourceOptions = [
248
+ {
249
+ id: ConversionSource.IMPORT_FILE,
250
+ text: ConversionSourceContext[ConversionSource.IMPORT_FILE]
251
+ },
252
+ {
253
+ id: ConversionSource.FROM_LEAD,
254
+ text: ConversionSourceContext[ConversionSource.FROM_LEAD]
255
+ },
256
+ {
257
+ id: ConversionSource.MANUAL,
258
+ text: ConversionSourceContext[ConversionSource.MANUAL]
259
+ },
260
+ {
261
+ id: ConversionSource.WORKFLOW,
262
+ text: ConversionSourceContext[ConversionSource.WORKFLOW]
263
+ },
264
+ {
265
+ id: ConversionSource.API,
266
+ text: ConversionSourceContext[ConversionSource.API]
267
+ },
268
+ {
269
+ id: ConversionSource.SYSTEM,
270
+ text: ConversionSourceContext[ConversionSource.SYSTEM]
271
+ }
272
+ ];
273
+ export {
274
+ AssigneeRoles,
275
+ ConversionSource,
276
+ ConversionSourceContext,
277
+ ConversionSourceOptions,
278
+ CrmType,
279
+ CustomerAssign,
280
+ CustomerType,
281
+ CustomerTypeContext,
282
+ DuplicateContactWarningType,
283
+ Gender,
284
+ GenderContext,
285
+ GenderOptions,
286
+ SeniorStaffRoles,
287
+ SourceUser,
288
+ SourceUserContext,
289
+ SourceUserOptions,
290
+ UserRole,
291
+ UserRoleContext,
292
+ isDuplicateContactWarning
293
+ };
@@ -0,0 +1,24 @@
1
+ import i18n from "../lib/i18n/index.js";
2
+ const ErrorMessage = Object.freeze({
3
+ ECONNABORTED: "Request b\u1ECB timeout",
4
+ ERR_NETWORK: "L\u1ED7i k\u1EBFt n\u1ED1i m\u1EA1ng",
5
+ ERR_BAD_REQUEST: "D\u1EEF li\u1EC7u request kh\xF4ng h\u1EE3p l\u1EC7",
6
+ ERR_BAD_RESPONSE: "Response kh\xF4ng h\u1EE3p l\u1EC7",
7
+ ERR_FR_TOO_MANY_REDIRECTS: "Qu\xE1 nhi\u1EC1u redirect",
8
+ ERR_CANCELED: "Request b\u1ECB h\u1EE7y",
9
+ ERR_INVALID_URL: "URL kh\xF4ng h\u1EE3p l\u1EC7"
10
+ });
11
+ class AxiosError {
12
+ constructor({ code, message = "" }) {
13
+ return {
14
+ error: true,
15
+ message: message || ErrorMessage[code] || i18n.t("settings:user.unknownError", {
16
+ defaultValue: "L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh"
17
+ }),
18
+ code
19
+ };
20
+ }
21
+ }
22
+ export {
23
+ AxiosError
24
+ };
@@ -0,0 +1,28 @@
1
+ import i18n from "../lib/i18n/index.js";
2
+ const ErrorMessage = Object.freeze({
3
+ 400: "D\u1EEF li\u1EC7u g\u1EEDi l\xEAn kh\xF4ng h\u1EE3p l\u1EC7",
4
+ 401: "B\u1EA1n kh\xF4ng c\xF3 quy\u1EC1n truy c\u1EADp",
5
+ 409: "\u0110\xE3 x\u1EA3y ra xung \u0111\u1ED9t, vui l\xF2ng th\u1EED l\u1EA1i sau!",
6
+ 403: "B\u1EA1n kh\xF4ng c\xF3 quy\u1EC1n truy c\u1EADp",
7
+ 404: "Ki\u1EC3m tra URL ho\u1EB7c endpoint c\xF3 \u0111\xFAng kh\xF4ng!",
8
+ 408: "Th\u1EDDi gian k\u1EBFt n\u1ED1i \u0111\u1EBFn server qu\xE1 l\xE2u, vui l\xF2ng ki\u1EC3m tra k\u1EBFt n\u1ED1i m\u1EA1ng!",
9
+ 429: "G\u1EEDi qu\xE1 nhi\u1EC1u y\xEAu c\u1EA7u \u0111\u1ED3ng th\u1EDDi, vui l\xF2ng th\u1EED l\u1EA1i sau \xEDt ph\xFAt!",
10
+ 500: "\u0110\xE3 x\u1EA3y ra l\u1ED7i t\u1EEB server, vui l\xF2ng th\u1EED l\u1EA1i sau!",
11
+ 503: "D\u1ECBch v\u1EE5 t\u1EA1m th\u1EDDi kh\xF4ng ho\u1EA1t \u0111\u1ED9ng, vui l\xF2ng th\u1EED l\u1EA1i sau!",
12
+ 504: "Server \u0111ang b\u1EADn, vui l\xF2ng th\u1EED l\u1EA1i sau!"
13
+ });
14
+ class HttpsError {
15
+ constructor({ status, message, data }) {
16
+ return {
17
+ error: true,
18
+ status,
19
+ data,
20
+ message: message || ErrorMessage[status] || i18n.t("common:label.anErrorOccurredDuringProcessing", {
21
+ defaultValue: "\u0110\xE3 x\u1EA3y ra l\u1ED7i trong qu\xE1 tr\xECnh th\u1EF1c hi\u1EC7n"
22
+ })
23
+ };
24
+ }
25
+ }
26
+ export {
27
+ HttpsError
28
+ };
@@ -0,0 +1,20 @@
1
+ class ValidatorError {
2
+ constructor({ validationErrors, message }) {
3
+ const errors = {};
4
+ if (validationErrors && validationErrors.length) {
5
+ validationErrors.filter((v) => v).forEach((err) => {
6
+ errors[err["param"]] = err["message"] || `${err["param"]} field is not a valid`;
7
+ });
8
+ }
9
+ return {
10
+ error: true,
11
+ data: errors,
12
+ message: message || "Validation Failed",
13
+ statusCode: 422,
14
+ isInvalid: true
15
+ };
16
+ }
17
+ }
18
+ export {
19
+ ValidatorError
20
+ };
@@ -0,0 +1,31 @@
1
+ const AVATAR_PALETTE = [
2
+ "#0EA5E9",
3
+ // sky
4
+ "#16A34A",
5
+ // green
6
+ "#F59E0B",
7
+ // amber
8
+ "#EC4899",
9
+ // pink
10
+ "#8B5CF6",
11
+ // violet
12
+ "#EF4444",
13
+ // red
14
+ "#06B6D4",
15
+ // cyan
16
+ "#F97316",
17
+ // orange
18
+ "#6366F1",
19
+ // indigo
20
+ "#14B8A6"
21
+ // teal
22
+ ];
23
+ function customerInitials(name) {
24
+ const parts = String(name ?? "").trim().split(/\s+/).filter(Boolean);
25
+ if (parts.length === 0) return "?";
26
+ if (parts.length === 1) return parts[0][0].toUpperCase();
27
+ return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
28
+ }
29
+ export {
30
+ customerInitials
31
+ };
@@ -0,0 +1,85 @@
1
+ import i18n from "i18next";
2
+ import LanguageDetector from "i18next-browser-languagedetector";
3
+ import ICU from "i18next-icu";
4
+ import resourcesToBackend from "i18next-resources-to-backend";
5
+ const SUPPORTED_LANGUAGES = ["vi", "en"];
6
+ function resolveFallback(code) {
7
+ return code === "vi" ? false : "en";
8
+ }
9
+ const enLoaders = {
10
+ "campaign": () => import("./locales/en/campaign.json"),
11
+ "chat": () => import("./locales/en/chat.json"),
12
+ "common": () => import("./locales/en/common.json"),
13
+ "customer": () => import("./locales/en/customer.json"),
14
+ "lead": () => import("./locales/en/lead.json"),
15
+ "lumi": () => import("./locales/en/lumi.json"),
16
+ "notification": () => import("./locales/en/notification.json"),
17
+ "org": () => import("./locales/en/org.json"),
18
+ "posts": () => import("./locales/en/posts.json"),
19
+ "settings": () => import("./locales/en/settings.json")
20
+ };
21
+ const viLoaders = {
22
+ "campaign": () => import("./locales/vi/campaign.json"),
23
+ "chat": () => import("./locales/vi/chat.json"),
24
+ "common": () => import("./locales/vi/common.json"),
25
+ "customer": () => import("./locales/vi/customer.json"),
26
+ "lead": () => import("./locales/vi/lead.json"),
27
+ "lumi": () => import("./locales/vi/lumi.json"),
28
+ "notification": () => import("./locales/vi/notification.json"),
29
+ "org": () => import("./locales/vi/org.json"),
30
+ "posts": () => import("./locales/vi/posts.json"),
31
+ "settings": () => import("./locales/vi/settings.json")
32
+ };
33
+ const loaders = { en: enLoaders, vi: viLoaders };
34
+ const NS = Object.keys(enLoaders).filter((ns) => ns !== "translation").sort();
35
+ async function loadNamespacesForLanguage(lng, namespaces) {
36
+ await i18n.reloadResources([lng], namespaces);
37
+ return i18n.getDataByLanguage(lng);
38
+ }
39
+ i18n.use(ICU).use(
40
+ resourcesToBackend((lng, ns) => {
41
+ const loader = loaders[lng]?.[ns];
42
+ if (!loader) return Promise.reject(new Error(`no locale ${lng}/${ns}`));
43
+ return loader().then((m) => m.default || m);
44
+ })
45
+ ).use(LanguageDetector).init({
46
+ ns: NS,
47
+ defaultNS: "common",
48
+ fallbackNS: "common",
49
+ partialBundledLanguages: true,
50
+ supportedLngs: SUPPORTED_LANGUAGES,
51
+ nonExplicitSupportedLngs: true,
52
+ // 'en-US' -> 'en', 'vi-VN' -> 'vi'
53
+ load: "languageOnly",
54
+ fallbackLng: resolveFallback,
55
+ // Namespaced keys `ns:section.leaf`. Split the namespace on ':'; the leaf
56
+ // (e.g. 'list.title') is a LITERAL flat key — dots are NOT path separators.
57
+ nsSeparator: ":",
58
+ keySeparator: false,
59
+ returnNull: false,
60
+ returnEmptyString: false,
61
+ interpolation: { escapeValue: false },
62
+ // React already escapes
63
+ detection: {
64
+ order: ["localStorage", "navigator"],
65
+ lookupLocalStorage: "lumi_lang",
66
+ caches: []
67
+ // language-store.js is the single writer
68
+ },
69
+ react: { useSuspense: false }
70
+ });
71
+ if (typeof document !== "undefined") {
72
+ const applyHtmlLang = (lng) => {
73
+ document.documentElement.setAttribute("lang", lng);
74
+ };
75
+ applyHtmlLang(i18n.resolvedLanguage || i18n.language);
76
+ i18n.on("languageChanged", applyHtmlLang);
77
+ }
78
+ var i18n_default = i18n;
79
+ export {
80
+ NS,
81
+ SUPPORTED_LANGUAGES,
82
+ i18n_default as default,
83
+ loadNamespacesForLanguage,
84
+ resolveFallback
85
+ };
@@ -0,0 +1,74 @@
1
+ import { getUser, setUser } from "../../utils/auth.js";
2
+ import { updateMyLanguage } from "../../api/user.js";
3
+ import { SUPPORTED_LANGUAGES } from "./index.js";
4
+ const STORAGE_KEY = "lumi_lang";
5
+ const languageStore = {
6
+ load() {
7
+ let user = null;
8
+ try {
9
+ user = getUser();
10
+ } catch {
11
+ }
12
+ if (user) {
13
+ return SUPPORTED_LANGUAGES.includes(user.language) ? user.language : "vi";
14
+ }
15
+ try {
16
+ return localStorage.getItem(STORAGE_KEY) || null;
17
+ } catch {
18
+ return null;
19
+ }
20
+ },
21
+ // Resolve the language for a user who has just logged in.
22
+ //
23
+ // `user.language` is the SINGLE SOURCE OF TRUTH. Two things are deliberately
24
+ // NOT consulted here:
25
+ //
26
+ // - The localStorage cache. `lumi_lang` is a DEVICE-level value that
27
+ // outlives logout, so reading it made one account inherit the previous
28
+ // account's language, and nothing rewrote it afterwards, so a refresh
29
+ // brought it straight back.
30
+ // - navigator.language. An earlier version fell back to the browser locale,
31
+ // which is 'en-US' for a great many Vietnamese users and therefore served
32
+ // English to the majority audience — the opposite of the intent.
33
+ //
34
+ // The backend now defaults user.language to 'vi' and every existing user was
35
+ // backfilled (contact-center-be migration/280726-backfill-user-language-vi.js),
36
+ // so the field is effectively always present. The 'vi' below is
37
+ // defence-in-depth for a document that predates the backfill or was written
38
+ // by another service — NOT a locale guess.
39
+ resolveForUser(user) {
40
+ const profileLang = user?.language;
41
+ return SUPPORTED_LANGUAGES.includes(profileLang) ? profileLang : "vi";
42
+ },
43
+ // Cache-only write. Unlike save(), this does NOT merge onto the stored
44
+ // account and does NOT PATCH the DB — persisting a browser-derived default
45
+ // would invent a preference the user never expressed.
46
+ cache(lang) {
47
+ try {
48
+ localStorage.setItem(STORAGE_KEY, lang);
49
+ } catch {
50
+ }
51
+ },
52
+ save(lang) {
53
+ try {
54
+ localStorage.setItem(STORAGE_KEY, lang);
55
+ } catch {
56
+ }
57
+ try {
58
+ const user = getUser();
59
+ if (user) setUser({ ...user, language: lang });
60
+ } catch {
61
+ }
62
+ try {
63
+ if (getUser()?.token) {
64
+ return updateMyLanguage(lang).catch(() => {
65
+ });
66
+ }
67
+ } catch {
68
+ }
69
+ return Promise.resolve();
70
+ }
71
+ };
72
+ export {
73
+ languageStore
74
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "field.messageType": "Message type"
3
+ }