l-min-components 1.7.1507 → 1.7.1509

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "l-min-components",
3
- "version": "1.7.1507",
3
+ "version": "1.7.1509",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src/assets",
@@ -170,10 +170,10 @@ const AppMainLayout = ({ children }) => {
170
170
  user,
171
171
  userDetails,
172
172
  handleGetFileStorageSummary,
173
- getFileStorageSummaryData,
173
+ // getFileStorageSummaryData,
174
174
  } = useHeader({ default: true });
175
175
 
176
- // get current default account and store in cookie (from api);
176
+ // Get current default account and store in cookie (from api);
177
177
  useEffect(() => {
178
178
  if (getDefaultAccount?.data) {
179
179
  const date = new Date();
@@ -197,6 +197,16 @@ const AppMainLayout = ({ children }) => {
197
197
 
198
198
  localStorage.setItem("defaultLang", getDefaultAccount?.data?.language);
199
199
 
200
+ const { registration_step } = getDefaultAccount?.data;
201
+ // If registration step is "REGISTRATION_COMPLETED" - go to acct type selection page
202
+ if (registration_step === "REGISTRATION_COMPLETED") {
203
+ window.location.href = "/auth/account-type";
204
+ }
205
+ // If registration step is "REGISTRATION_PARTIALLY_COMPLETED" go to /register where the user can provide the missing fields.
206
+ else if (registration_step === "REGISTRATION_PARTIALLY_COMPLETED") {
207
+ window.location.href = "/auth/register";
208
+ }
209
+
200
210
  // set default acct when you receive the default acct data
201
211
  handleSetDefaultAccount(getDefaultAccount?.data?.id);
202
212
 
@@ -237,7 +247,6 @@ const AppMainLayout = ({ children }) => {
237
247
  // Use setTimeout to ensure navigation happens after React Router is ready
238
248
  setTimeout(() => {
239
249
  const targetUrl = `/${defaultAccountType}`;
240
- console.log(`Navigating to: ${targetUrl}`);
241
250
 
242
251
  // Try using window.location.replace for staging compatibility
243
252
  if (window.location.hostname.includes("staging")) {
@@ -245,7 +254,7 @@ const AppMainLayout = ({ children }) => {
245
254
  } else {
246
255
  window.location.href = targetUrl;
247
256
  }
248
- }, 100);
257
+ }, 1000);
249
258
  }
250
259
  }
251
260
  }
@@ -311,16 +320,6 @@ const AppMainLayout = ({ children }) => {
311
320
  handleGetDefaultAccount();
312
321
  }, [defaultAcct]);
313
322
 
314
- // Debug the API response
315
- useEffect(() => {
316
- console.log("getDefaultAccount state changed:", {
317
- data: getDefaultAccount?.data,
318
- loading: getDefaultAccount?.loading,
319
- error: getDefaultAccount?.error,
320
- response: getDefaultAccount?.response,
321
- });
322
- }, [getDefaultAccount]);
323
-
324
323
  // setting current environment type
325
324
  useEffect(() => {
326
325
  if (window.location.hostname.includes("staging")) {
@@ -384,6 +383,50 @@ const AppMainLayout = ({ children }) => {
384
383
  "Build the future with Learngual!",
385
384
  "Choose our custom plans for more calls and better flexibility.",
386
385
  "We know it's inconvenient. For better user accessibility, login with a desktop device.",
386
+
387
+ // Additional text from codebase analysis - ONLY actual strings found
388
+ "Student",
389
+ "Instructor account",
390
+ "Enterprise account",
391
+ "Student account",
392
+ "Dashboard",
393
+ "Courses",
394
+ "My courses",
395
+ "Manage courses",
396
+ "Manage report",
397
+ "Manage teams",
398
+ "Manage student",
399
+ "Messages",
400
+ "Announcements",
401
+ "File manager",
402
+ "Library",
403
+ "Add on",
404
+ "Contracts",
405
+ "Demo",
406
+ "Explore",
407
+ "Connect",
408
+ "Select affiliate",
409
+ "Search affiliates",
410
+ "Affiliates",
411
+ "GRACE PERIOD",
412
+ "ACTIVE",
413
+ "Oops!",
414
+ "Select plans",
415
+ "Renew subscription",
416
+ "Instructor",
417
+ "Learngual logo",
418
+ "profile",
419
+ "account photo",
420
+ "Select Plans",
421
+ "Search by Name / Email / Username",
422
+ "Text",
423
+ "add",
424
+ "Ready",
425
+ "play",
426
+
427
+ // expired instructor
428
+ "You haven’t been added to an enterprise account yet",
429
+ "This account will be active once an enterprise assigns you as an instructor.",
387
430
  ];
388
431
 
389
432
  const {
@@ -578,6 +621,7 @@ const AppMainLayout = ({ children }) => {
578
621
  onChange={(v) => setAffiliateAccount(v)}
579
622
  setSearch={setAffilitateSearch}
580
623
  defaultAffiliate={getDefaultAffiliateData?.data}
624
+ findText={findText}
581
625
  />
582
626
  </>
583
627
  )}
@@ -608,6 +652,7 @@ const AppMainLayout = ({ children }) => {
608
652
  gracePeriod={gracePeriod}
609
653
  stateType={planState === "GRACE PERIOD" ? 1 : 2}
610
654
  planState={planState}
655
+ findText={findText}
611
656
  />
612
657
  ) : activeAccountType === "instructor" &&
613
658
  !userPlanData?.loading &&
@@ -620,17 +665,19 @@ const AppMainLayout = ({ children }) => {
620
665
  !getAllAffiliateData?.loading &&
621
666
  !window.location.pathname.includes("notif") ? (
622
667
  <div className="instructor_expired">
623
- <h1>Dashboard</h1>
668
+ <h1>{findText("Dashboard")}</h1>
624
669
  <div className="instructor_expired_body">
625
670
  <div className="instructor_expired_center">
626
671
  <img src={instructorImage} alt="" />
627
672
  <h4>
628
- You haven’t been added to an enterprise
629
- account yet
673
+ {findText(
674
+ "You haven’t been added to an enterprise account yet"
675
+ )}
630
676
  </h4>
631
677
  <p>
632
- This account will be active once an enterprise
633
- assigns you as an instructor.
678
+ {findText(
679
+ "This account will be active once an enterprise assigns you as an instructor."
680
+ )}
634
681
  </p>
635
682
  </div>
636
683
  </div>
@@ -22,6 +22,7 @@ const GracePeriod = ({
22
22
  handleCurrentSubscription,
23
23
  gracePeriod,
24
24
  stateType,
25
+ findText,
25
26
  }) => {
26
27
  console.log("🚀 ~ gracePeriod:", gracePeriod);
27
28
  const location = useLocation();
@@ -80,7 +81,7 @@ const GracePeriod = ({
80
81
 
81
82
  return (
82
83
  <GracePeriodWrapper>
83
- <h1>{pageTitle}</h1>
84
+ <h1>{findText(pageTitle)}</h1>
84
85
  {stateType === 1 ? (
85
86
  <div
86
87
  className="grace_period_body"
@@ -94,26 +95,26 @@ const GracePeriod = ({
94
95
  }}
95
96
  >
96
97
  <img src={warning} alt="" />
97
- <h2>Your subscription to Learngual has expired</h2>
98
- <p>
99
- Your students and instructors will no longer <br />
100
- be able to access your courses after <br />
101
- <span
102
- style={{
103
- fontWeight: 700,
104
- color: "red",
105
- }}
106
- >
107
- {timeLeft}{" "}
108
- </span>
109
- </p>
98
+ <h2>{findText("Your subscription to Learngual has expired")}</h2>
99
+ <p
100
+ dangerouslySetInnerHTML={{
101
+ __html: findText(
102
+ "Your students and instructors will no longer {br1} be able to access your courses after {br2} {span}",
103
+ {
104
+ br1: `<br>`,
105
+ br2: `<br>`,
106
+ span: `<span style="fontWeight: 700; color: red;">${timeLeft}</span>`,
107
+ }
108
+ ),
109
+ }}
110
+ ></p>
110
111
  <div
111
112
  onClick={() => {
112
113
  window.location.href = "/settings/payment";
113
114
  }}
114
115
  >
115
116
  <Button
116
- text="Renew subscription"
117
+ text={findText("Renew subscription")}
117
118
  styles={{
118
119
  height: "35px",
119
120
  padding: "4px 20px",
@@ -125,10 +126,11 @@ const GracePeriod = ({
125
126
  ) : (
126
127
  <>
127
128
  <Expire>
128
- <h4>Your subscription has expired</h4>
129
+ <h4>{findText("Your subscription has expired")}</h4>
129
130
  <h5>
130
- You don’t have an active plan, renew your subscription to gain
131
- full access to your account.
131
+ {findText(
132
+ "You don’t have an active plan, renew your subscription to gain full access to your account."
133
+ )}
132
134
  </h5>
133
135
 
134
136
  <div
@@ -137,7 +139,7 @@ const GracePeriod = ({
137
139
  }}
138
140
  >
139
141
  <Button
140
- text="Choose plan"
142
+ text={findText("Choose plan")}
141
143
  styles={{
142
144
  height: "35px",
143
145
  padding: "4px 20px",
@@ -155,7 +157,7 @@ const GracePeriod = ({
155
157
  }}
156
158
  />
157
159
  <div className="content">
158
- <p>Sorry, you can’t access your account</p>
160
+ <p>{findText("Sorry, you can’t access your account")}</p>
159
161
  </div>
160
162
  </div>
161
163
  </>
@@ -75,10 +75,6 @@ const SubscriptionExpiredModal = ({
75
75
  // };
76
76
  // }, []);
77
77
 
78
- const noSubError = () => {
79
- toast.error("Error getting your subscription, please try again!");
80
- };
81
-
82
78
  const subId = getCurrentSubscriptionData?.data?.id;
83
79
 
84
80
  const handleNavigateRenewal = () => {
@@ -22,8 +22,8 @@ const Index = () => {
22
22
  <h3>Opss...</h3>
23
23
  <h2>Page not found</h2>
24
24
  <p>
25
- Seems we can't find the page your looking for. Let's
26
- get you back home.
25
+ Seems we can't find the page your looking for. Let's get you back
26
+ home.
27
27
  </p>
28
28
  <ButtonComponent
29
29
  text="Go Back"
@@ -3,9 +3,10 @@ import styled from "styled-components";
3
3
 
4
4
  import logo from "../../assets/images/logo.png";
5
5
  import bg from "./images/bg-404.png";
6
- import redirectURL from "../../utils/redirectURL";
7
6
  import useCustomNavigate from "../../hooks/useCustomNavigate";
8
7
  import getCookie from "../../utils/getCookie";
8
+ import useTranslation from "../../hooks/useTranslation";
9
+ import FullPageLoader from "../fullPageLoader";
9
10
 
10
11
  const ErrorWrapper = ({
11
12
  title = "Oops!",
@@ -31,7 +32,21 @@ const ErrorWrapper = ({
31
32
  navigate(fallbackPath || "/", { reload: true });
32
33
  }
33
34
  };
34
- return (
35
+
36
+ const wordBank = [
37
+ "Opss...",
38
+ "Page not found",
39
+ "Seems we can't find the page your looking for. Let's get you back home.",
40
+ "Go Back",
41
+ "There was an error",
42
+ ];
43
+
44
+ const { findText, translations, isTranslationsLoading } =
45
+ useTranslation(wordBank);
46
+
47
+ return isTranslationsLoading && Object.keys(translations)?.length === 0 ? (
48
+ <FullPageLoader hasBackground fixed={true} />
49
+ ) : (
35
50
  <Container>
36
51
  <Navbar>
37
52
  <a href="/">
@@ -40,13 +55,13 @@ const ErrorWrapper = ({
40
55
  </Navbar>
41
56
  <Content>
42
57
  <LeftDiv>
43
- <h1>{title}</h1>
58
+ <h1>{findText(title)}</h1>
44
59
  <div>
45
- {subTitle && <h2>{subTitle}</h2>}
46
- {message && <p>{message}</p>}
60
+ {subTitle && <h2>{findText(subTitle)}</h2>}
61
+ {message && <p>{findText(message)}</p>}
47
62
  </div>
48
63
 
49
- <button onClick={handleNav}>{btnText}</button>
64
+ <button onClick={handleNav}>{findText(btnText)}</button>
50
65
  </LeftDiv>
51
66
  <ImageBox>
52
67
  <img src={bgSrc || bg} alt="" />
@@ -183,7 +183,9 @@ const AccountDropdown = (props) => {
183
183
  <>
184
184
  {props?.instructorAccountData?.length > 0 && (
185
185
  <div>
186
- <h3 style={{ marginBottom: 10 }}>Instructor account</h3>
186
+ <h3 style={{ marginBottom: 10 }}>
187
+ {props.findText("Instructor account")}
188
+ </h3>
187
189
  {props?.instructorAccountData?.map((instructorItem, idx) => {
188
190
  console.log("account>>", instructorItem);
189
191
  return (
@@ -233,7 +235,9 @@ const AccountDropdown = (props) => {
233
235
  )}
234
236
  {props?.enterpriseAccountData?.length > 0 && (
235
237
  <div>
236
- <h3 style={{ marginBottom: 10 }}>Enterprise account</h3>
238
+ <h3 style={{ marginBottom: 10 }}>
239
+ {props?.findText("Enterprise account")}
240
+ </h3>
237
241
  {props?.enterpriseAccountData?.map((enterpriseItem, idx) => (
238
242
  <div
239
243
  className={`account-info ${
@@ -279,7 +283,9 @@ const AccountDropdown = (props) => {
279
283
  // dont show personal accounts for developer user
280
284
  props?.personalAccountData?.length > 0 && (
281
285
  <div>
282
- <h3 style={{ marginBottom: 10 }}>Student account</h3>
286
+ <h3 style={{ marginBottom: 10 }}>
287
+ {props?.findText("Student account")}
288
+ </h3>
283
289
  {props?.personalAccountData?.map((personalItem, idx) => (
284
290
  <div
285
291
  className={`account-info ${
@@ -329,7 +335,10 @@ const AccountDropdown = (props) => {
329
335
  <AccountDropdownFooter>
330
336
  <button
331
337
  onClick={() => {
332
- window.location.href = "/auth/account-type";
338
+ window.location.hostname?.includes("coming")
339
+ ? (window.location.href =
340
+ "https://learngual.com/auth/account-type")
341
+ : (window.location.href = "/auth/account-type");
333
342
  }}
334
343
  style={{ cursor: "pointer" }}
335
344
  >
@@ -552,8 +552,10 @@ const HeaderComponent = (props) => {
552
552
  <h5 style={{ textTransform: "capitalize" }}>{accountName}</h5>
553
553
  <h6 style={{ textTransform: "capitalize" }}>
554
554
  {props?.selectedAccount?.type?.toLowerCase() === "personal"
555
- ? "Student"
556
- : props?.selectedAccount?.type?.toLowerCase()}
555
+ ? props?.findtext("Student")
556
+ : props?.findtext(
557
+ props?.selectedAccount?.type?.toLowerCase()
558
+ )}
557
559
  </h6>
558
560
  </div>
559
561
  <ArrowDownIcon width={16} height={10} />
@@ -7,11 +7,11 @@ export const languagesData = [
7
7
  flag: usFlag,
8
8
  slug: "en",
9
9
  },
10
- // {
11
- // name: "Korean",
12
- // flag: koreanFlag,
13
- // slug: "ko",
14
- // },
10
+ {
11
+ name: "Korean",
12
+ flag: koreanFlag,
13
+ slug: "ko",
14
+ },
15
15
  ];
16
16
 
17
17
  export const selectedLanguageData = {
@@ -33,11 +33,11 @@ const RenewModal = ({
33
33
  <BtnGroup>
34
34
  <Button
35
35
  type="secondary"
36
- text={btn1Text || "Select Plans"}
36
+ text={btn1Text || "Select plans"}
37
37
  onClick={btn1onClick}
38
38
  />
39
39
  <Button
40
- text={btn2Text || "Renew Subscription"}
40
+ text={btn2Text || "Renew subscription"}
41
41
  onClick={btn2onClick}
42
42
  />
43
43
  </BtnGroup>
@@ -17,6 +17,7 @@ const InstructorAccountSwitcher = ({
17
17
  affiliateList,
18
18
  setSearch,
19
19
  defaultAffiliate,
20
+ findText,
20
21
  }) => {
21
22
  // const [expiryFlow, setExpiryFlow] = useState();
22
23
  const [switchValue, setSwitchValue] = useState("affiliates");
@@ -169,7 +170,7 @@ const InstructorAccountSwitcher = ({
169
170
  className="placeholder"
170
171
  onClick={() => setDropdown(!dropdown)}
171
172
  >
172
- <p>Select affiliate</p>
173
+ <p>{findText("Select affiliate")}</p>
173
174
  <ArrowDown />
174
175
  </div>
175
176
  ) : (
@@ -189,7 +190,7 @@ const InstructorAccountSwitcher = ({
189
190
  <AffiliatesDropDownWrapper>
190
191
  <div className="search_wrapper">
191
192
  <Search
192
- placeholder="Search Affiliates"
193
+ placeholder={findText("Search affiliates")}
193
194
  onSubmit={setSearch}
194
195
  />
195
196
  </div>
@@ -205,7 +206,9 @@ const InstructorAccountSwitcher = ({
205
206
  <img src={item?.image} alt="" />
206
207
  <p>{item?.name}</p>
207
208
  </div>
208
- {item.count ? <span>{item.count}</span> : null}
209
+ {item.count ? (
210
+ <span>{findText(item.count)}</span>
211
+ ) : null}
209
212
  </li>
210
213
  ))}
211
214
  </ul>
@@ -222,7 +225,7 @@ const InstructorAccountSwitcher = ({
222
225
  className={switchValue !== "affiliates" ? "active" : ""}
223
226
  onClick={() => handleSwitch(1)}
224
227
  >
225
- <span>Personal</span>
228
+ <span>{findText("Personal")}</span>
226
229
  <span className="circle"></span>
227
230
  </button>
228
231
  )}
@@ -230,7 +233,7 @@ const InstructorAccountSwitcher = ({
230
233
  className={switchValue === "affiliates" ? "active" : ""}
231
234
  onClick={() => handleSwitch(2)}
232
235
  >
233
- <span>Affiliates</span>
236
+ <span>{findText("Affiliates")}</span>
234
237
  <span className="circle"></span>
235
238
  </button>
236
239
  </div>
@@ -23,7 +23,10 @@ import {
23
23
  AccountsIconActive,
24
24
  } from "../assets/adminSvg/accountsIcon";
25
25
  import { ManagersIcon, ManagersIconActive } from "../assets/adminSvg/managers";
26
- import { RequestsIcon, RequestsIconActive } from "../assets/adminSvg/requestsIcon";
26
+ import {
27
+ RequestsIcon,
28
+ RequestsIconActive,
29
+ } from "../assets/adminSvg/requestsIcon";
27
30
 
28
31
  export const adminSideMenuOptions = [
29
32
  {
@@ -139,25 +142,25 @@ export const adminSideMenuOptions = [
139
142
  path: "/personal-account",
140
143
  hasNotification: false,
141
144
  notifications: null,
142
- text: "Personal Account",
145
+ text: "Personal account",
143
146
  },
144
147
  {
145
148
  path: "/instructor-account",
146
149
  hasNotification: false,
147
150
  notifications: null,
148
- text: "Instructor Account",
151
+ text: "Instructor account",
149
152
  },
150
153
  {
151
154
  path: "/enterprise-account",
152
155
  hasNotification: false,
153
156
  notifications: null,
154
- text: "Enterprise Account",
157
+ text: "Enterprise account",
155
158
  },
156
159
  {
157
160
  path: "/developer-account",
158
161
  hasNotification: false,
159
162
  notifications: null,
160
- text: "Developer Account",
163
+ text: "Developer account",
161
164
  },
162
165
  ],
163
166
  },
@@ -220,7 +223,7 @@ export const adminSideMenuOptions = [
220
223
  path: "/all-users",
221
224
  icon: <UsersIcon />,
222
225
  iconActive: <UsersIconActive />,
223
- text: "All Users",
226
+ text: "All users",
224
227
  hasNotification: false,
225
228
  hasDropdown: false,
226
229
  notifications: null,
@@ -229,7 +232,7 @@ export const adminSideMenuOptions = [
229
232
  ],
230
233
  },
231
234
  {
232
- optionType: "Dev. Management",
235
+ optionType: "Dev. management",
233
236
  routes: [
234
237
  {
235
238
  path: "/accounts",
@@ -1,6 +1,15 @@
1
1
  import React, { useCallback, useContext, useEffect, useState } from "react";
2
2
  import useAxios from "axios-hooks";
3
3
 
4
+ // Global cache for master translations
5
+ if (!window.translationCache) {
6
+ window.translationCache = {};
7
+ }
8
+
9
+ const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
10
+ const S3_BASE_URL =
11
+ "https://learngual-bucket.sfo3.digitaloceanspaces.com/media/media/";
12
+
4
13
  const useTranslation = (initialSentences = []) => {
5
14
  const value = localStorage?.getItem("defaultLang");
6
15
 
@@ -14,7 +23,7 @@ const useTranslation = (initialSentences = []) => {
14
23
  }, [value]);
15
24
 
16
25
  const [defaultLang, setDefaultLang] = useState(value ?? "en");
17
- const [translations, setTranslations] = useState({}); // returned translation from backend
26
+ const [translations, setTranslations] = useState({}); // returned translation from S3
18
27
  const [isTranslationsLoading, setIsTranslationsLoading] = useState(true);
19
28
 
20
29
  const findText = useCallback(
@@ -24,7 +33,7 @@ const useTranslation = (initialSentences = []) => {
24
33
  [translations]
25
34
  );
26
35
 
27
- // api request for sending data to backend
36
+ // api request for sending data to backend (fire-and-forget)
28
37
  const [{ ...translateData }, translate] = useAxios(
29
38
  {
30
39
  method: "POST",
@@ -34,14 +43,17 @@ const useTranslation = (initialSentences = []) => {
34
43
  }
35
44
  );
36
45
 
37
- const handleTranslate = async (language, words) => {
46
+ /**
47
+ * Send translation request to backend (fire-and-forget)
48
+ */
49
+ const sendTranslationRequestToBackend = async (language, words) => {
38
50
  try {
39
- await translate({
51
+ // Fire-and-forget - don't await or use response
52
+ translate({
40
53
  url: `/iam/v1/utils/translate/`,
41
54
  data: {
42
55
  language,
43
56
  sentences: words,
44
- // kwargs,
45
57
  },
46
58
  params: {
47
59
  _account: "",
@@ -51,38 +63,135 @@ const useTranslation = (initialSentences = []) => {
51
63
  },
52
64
  });
53
65
  } catch (err) {
54
- console.log(err);
66
+ console.warn("Translation request failed:", err);
55
67
  }
56
68
  };
57
69
 
58
- useEffect(() => {
59
- handleTranslate(defaultLang, initialSentences);
60
- }, [defaultLang]);
70
+ /**
71
+ * Fetch master translations from S3 with caching
72
+ */
73
+ const getMasterTranslationsWithCache = async () => {
74
+ const now = Date.now();
75
+ const cached = window.translationCache.master;
61
76
 
62
- useEffect(() => {
63
- if (translateData?.data) {
64
- const newTranslations = {};
65
- initialSentences.map((word, index) => {
66
- newTranslations[word] = translateData?.data?.result[index];
77
+ // Check if cache is valid and not expired
78
+ if (cached && cached.data && cached.expires > now) {
79
+ return cached.data;
80
+ }
81
+
82
+ try {
83
+ // Fetch from S3 - master JSON contains all languages
84
+ const response = await fetch(
85
+ `${S3_BASE_URL}qFpINMa05DrgUgzO0PEboReejximpq2r3VL2AmFZAwIq6fTN3A.json`
86
+ );
87
+
88
+ if (!response.ok) {
89
+ throw new Error(`Failed to fetch translations: ${response.status}`);
90
+ }
91
+
92
+ const masterTranslations = await response.json();
93
+
94
+ console.log(masterTranslations, "MASTER");
95
+
96
+ // Cache in memory (single cache for all languages since JSON contains all)
97
+ window.translationCache.master = {
98
+ data: masterTranslations,
99
+ timestamp: now,
100
+ expires: now + CACHE_DURATION,
101
+ };
102
+
103
+ return masterTranslations;
104
+ } catch (error) {
105
+ console.error("Failed to fetch master translations:", error);
106
+
107
+ // Fallback to cached data if available (even if expired)
108
+ if (cached && cached.data) {
109
+ return cached.data;
110
+ }
111
+
112
+ throw error;
113
+ }
114
+ };
115
+
116
+ /**
117
+ * Extract relevant translations for specific texts
118
+ */
119
+ const extractRelevantTranslations = (masterTranslations, words, language) => {
120
+ const relevantTranslations = {};
121
+ const languageCode = language.toUpperCase(); // ko -> KO, fr -> FR
122
+
123
+ words.forEach((word) => {
124
+ if (masterTranslations[word] && masterTranslations[word][languageCode]) {
125
+ const translation = masterTranslations[word][languageCode];
126
+ // Only use translation if it's not empty
127
+ relevantTranslations[word] = translation.trim() || word;
128
+ } else {
129
+ // Return original text if translation not found
130
+ relevantTranslations[word] = word;
131
+ }
132
+ });
133
+
134
+ return relevantTranslations;
135
+ };
136
+
137
+ const handleTranslate = async (language, words) => {
138
+ if (!words || words.length === 0) {
139
+ setIsTranslationsLoading(false);
140
+ return;
141
+ }
142
+
143
+ try {
144
+ // Step 1: Fire-and-forget translation request to backend
145
+ sendTranslationRequestToBackend(language, words);
146
+
147
+ // Step 2: Get master translations (cached or fresh from S3)
148
+ const masterTranslations = await getMasterTranslationsWithCache();
149
+
150
+ // Step 3: Extract only the translations we need
151
+ const relevantTranslations = extractRelevantTranslations(
152
+ masterTranslations,
153
+ words,
154
+ language
155
+ );
156
+
157
+ // Step 4: Update state
158
+ setTranslations((prevTranslations) => ({
159
+ ...prevTranslations,
160
+ ...relevantTranslations,
161
+ }));
162
+
163
+ setIsTranslationsLoading(false);
164
+ } catch (error) {
165
+ console.error("Failed to load translations:", error);
166
+
167
+ // Fallback: return original texts
168
+ const fallbackTranslations = {};
169
+ words.forEach((word) => {
170
+ fallbackTranslations[word] = word;
67
171
  });
172
+
68
173
  setTranslations((prevTranslations) => ({
69
174
  ...prevTranslations,
70
- ...newTranslations,
175
+ ...fallbackTranslations,
71
176
  }));
177
+
178
+ setIsTranslationsLoading(false);
72
179
  }
73
- }, [translateData?.data]);
180
+ };
74
181
 
75
182
  useEffect(() => {
76
- if (Object.keys(translations)?.length > 0) {
183
+ if (initialSentences && initialSentences.length > 0) {
184
+ handleTranslate(defaultLang, initialSentences);
185
+ } else {
77
186
  setIsTranslationsLoading(false);
78
187
  }
79
- }, [translations]);
80
- console.log("translateData", translateData);
188
+ }, [defaultLang]);
81
189
 
190
+ // Remove the old useEffect that processed translateData.data since we don't use API response anymore
82
191
  return {
83
192
  defaultLang,
84
193
  setDefaultLang,
85
- translateData,
194
+ translateData, // Keep for backward compatibility (though not used anymore)
86
195
  handleTranslate,
87
196
  translations,
88
197
  findText,
@@ -1,6 +1,142 @@
1
1
  import { db } from "./db";
2
2
  import axios from "./axiosConfig";
3
3
 
4
+ // Global cache for master translations
5
+ if (!window.translationCache) {
6
+ window.translationCache = {};
7
+ }
8
+
9
+ const CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
10
+ const S3_BASE_URL =
11
+ "https://learngual-bucket.sfo3.digitaloceanspaces.com/media/media/";
12
+
13
+ /**
14
+ * Send translation request to backend (fire-and-forget)
15
+ */
16
+ async function sendTranslationRequest(texts, kwargs, language) {
17
+ try {
18
+ axios({
19
+ url: "/iam/v1/utils/translate/",
20
+ method: "POST",
21
+ data: {
22
+ language,
23
+ sentences: texts,
24
+ kwargs,
25
+ },
26
+ params: {
27
+ _account: "",
28
+ },
29
+ authRequired: false,
30
+ });
31
+ } catch (error) {
32
+ // Fire-and-forget, ignore errors
33
+ console.warn("Translation request failed:", error);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Fetch master translations from S3 with caching
39
+ * Since the JSON contains all languages, we cache it once regardless of language
40
+ */
41
+ async function getMasterTranslationsWithCache() {
42
+ const now = Date.now();
43
+ const cached = window.translationCache.master;
44
+
45
+ // Check if cache is valid and not expired
46
+ if (cached && cached.data && cached.expires > now) {
47
+ return cached.data;
48
+ }
49
+
50
+ try {
51
+ // Fetch from S3 - master JSON contains all languages
52
+ const response = await fetch(
53
+ `${S3_BASE_URL}qFpINMa05DrgUgzO0PEboReejximpq2r3VL2AmFZAwIq6fTN3A.json`
54
+ );
55
+
56
+ if (!response.ok) {
57
+ throw new Error(`Failed to fetch translations: ${response.status}`);
58
+ }
59
+
60
+ const masterTranslations = await response.json();
61
+
62
+ // Cache in memory (single cache for all languages since JSON contains all)
63
+ window.translationCache.master = {
64
+ data: masterTranslations,
65
+ timestamp: now,
66
+ expires: now + CACHE_DURATION,
67
+ };
68
+
69
+ console.log(masterTranslations, "master");
70
+
71
+ return masterTranslations;
72
+ } catch (error) {
73
+ console.error("Failed to fetch master translations:", error);
74
+
75
+ // Fallback to cached data if available (even if expired)
76
+ if (cached && cached.data) {
77
+ return cached.data;
78
+ }
79
+
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Extract relevant translations for specific texts
86
+ * Supports multiple languages: ko -> KO, fr -> FR, etc.
87
+ */
88
+ function extractRelevantTranslations(masterTranslations, texts, language) {
89
+ const relevantTranslations = {};
90
+ const languageCode = language.toUpperCase(); // ko -> KO, fr -> FR
91
+
92
+ texts.forEach((text) => {
93
+ if (masterTranslations[text] && masterTranslations[text][languageCode]) {
94
+ const translation = masterTranslations[text][languageCode];
95
+ // Only use translation if it's not empty
96
+ relevantTranslations[text] = translation.trim() || text;
97
+ } else {
98
+ // Return original text if translation not found
99
+ relevantTranslations[text] = text;
100
+ }
101
+ });
102
+
103
+ return relevantTranslations;
104
+ }
105
+
106
+ /**
107
+ * Cache screen-specific translations in IndexedDB
108
+ */
109
+ async function cacheScreenTranslations(translations, screenId, language) {
110
+ try {
111
+ const translationEntries = Object.keys(translations).map((key) => ({
112
+ key,
113
+ value: translations[key],
114
+ screenId,
115
+ updatedAt: new Date().toDateString(),
116
+ }));
117
+
118
+ await db[language].bulkPut(translationEntries);
119
+ } catch (error) {
120
+ console.error("Failed to cache translations:", error);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get cached translations from IndexedDB as fallback
126
+ */
127
+ async function getCachedTranslations(screenId, language) {
128
+ try {
129
+ const cached = await db[language].where({ screenId }).toArray();
130
+ return cached.reduce((acc, item) => {
131
+ acc[item.key] = item.value;
132
+ return acc;
133
+ }, {});
134
+ } catch (error) {
135
+ console.error("Failed to get cached translations:", error);
136
+ return {};
137
+ }
138
+ }
139
+
4
140
  async function loadTranslations(language = "ko") {
5
141
  await new Promise((resolve) => {
6
142
  //check if loading else resolve
@@ -25,38 +161,42 @@ async function loadTranslations(language = "ko") {
25
161
  */
26
162
  const kwargList = await db.kwarg.where({ screenId }).toArray();
27
163
  const kwargs = kwargList.reduce((x, t) => ({ ...x, [t.key]: t.value }), {});
28
- /**
29
- * @type {{ data: {has_translated: boolean; result: string[]} }}
30
- */
31
- const res = await axios({
32
- url: "/iam/v1/utils/translate/",
33
- method: "POST",
34
- data: {
35
- language,
36
- sentences: texts,
37
- kwargs,
38
- },
39
- params: {
40
- _account: "",
41
- },
42
- authRequired: false,
43
- });
44
- // build an object using texts as key and results from request as value
45
- if (!res.data) return {}; // somehow data was not found
46
- const translations = texts.reduce(
47
- (x, y, z) => ({ ...x, [y]: res.data.result[z] }),
48
- {}
49
- );
50
- db[language].bulkPut(
51
- Object.keys(translations).map((x, i) => ({
52
- key: texts[i],
53
- value: translations[x],
54
- screenId,
55
- updatedAt: new Date().toDateString(),
56
- }))
57
- );
58
164
 
59
- return translations;
165
+ // Step 1: Fire-and-forget translation request to backend
166
+ sendTranslationRequest(texts, kwargs, language);
167
+
168
+ try {
169
+ // Step 2: Get master translations (cached or fresh from S3)
170
+ const masterTranslations = await getMasterTranslationsWithCache();
171
+
172
+ // Step 3: Extract only the translations we need
173
+ const relevantTranslations = extractRelevantTranslations(
174
+ masterTranslations,
175
+ texts,
176
+ language
177
+ );
178
+
179
+ // Step 4: Cache the extracted translations in IndexedDB
180
+ await cacheScreenTranslations(relevantTranslations, screenId, language);
181
+
182
+ return relevantTranslations;
183
+ } catch (error) {
184
+ console.error("Failed to load translations:", error);
185
+
186
+ // Step 5: Fallback to cached translations from IndexedDB
187
+ const cachedTranslations = await getCachedTranslations(screenId, language);
188
+
189
+ // If we have cached translations, return them
190
+ if (Object.keys(cachedTranslations).length > 0) {
191
+ return cachedTranslations;
192
+ }
193
+
194
+ // Last resort: return original texts
195
+ return texts.reduce((acc, text) => {
196
+ acc[text] = text;
197
+ return acc;
198
+ }, {});
199
+ }
60
200
  }
61
201
 
62
202
  export function extractBracedText(text) {