@churchapps/apphelper 0.4.49 → 0.5.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 (102) hide show
  1. package/dist/components/FormCardPayment.d.ts +1 -1
  2. package/dist/components/FormCardPayment.d.ts.map +1 -1
  3. package/dist/components/FormCardPayment.js +27 -10
  4. package/dist/components/FormCardPayment.js.map +1 -1
  5. package/dist/components/Loading.js +36 -36
  6. package/dist/components/notes/Notes.js +27 -27
  7. package/dist/helpers/index.d.ts +1 -1
  8. package/dist/helpers/index.d.ts.map +1 -1
  9. package/dist/helpers/index.js +1 -1
  10. package/dist/helpers/index.js.map +1 -1
  11. package/dist/public/css/cropper.css +309 -309
  12. package/dist/public/css/styles.css +111 -111
  13. package/package.json +72 -73
  14. package/public/css/cropper.css +309 -309
  15. package/public/css/styles.css +111 -111
  16. package/public/locales/de.json +269 -269
  17. package/public/locales/en.json +276 -276
  18. package/public/locales/es.json +272 -272
  19. package/public/locales/fr.json +269 -269
  20. package/public/locales/hi.json +269 -269
  21. package/public/locales/it.json +269 -269
  22. package/public/locales/ko.json +269 -269
  23. package/public/locales/no.json +269 -269
  24. package/public/locales/pt.json +269 -269
  25. package/public/locales/ru.json +269 -269
  26. package/public/locales/tl.json +269 -269
  27. package/public/locales/zh.json +269 -269
  28. package/src/components/DisplayBox.tsx +83 -83
  29. package/src/components/ErrorMessages.tsx +28 -28
  30. package/src/components/ExportLink.tsx +81 -81
  31. package/src/components/FloatingSupport.tsx +18 -18
  32. package/src/components/FormCardPayment.tsx +184 -169
  33. package/src/components/FormSubmissionEdit.tsx +168 -168
  34. package/src/components/HelpIcon.tsx +12 -12
  35. package/src/components/ImageEditor.tsx +161 -161
  36. package/src/components/InputBox.tsx +96 -96
  37. package/src/components/Loading.tsx +77 -77
  38. package/src/components/PageHeader.tsx +110 -110
  39. package/src/components/PersonAvatar.tsx +77 -77
  40. package/src/components/QuestionEdit.tsx +99 -99
  41. package/src/components/SmallButton.tsx +42 -42
  42. package/src/components/SupportModal.tsx +32 -32
  43. package/src/components/TabPanel.tsx +28 -28
  44. package/src/components/gallery/GalleryModal.tsx +173 -173
  45. package/src/components/gallery/StockPhotos.tsx +95 -95
  46. package/src/components/gallery/index.ts +1 -1
  47. package/src/components/header/Banner.tsx +11 -11
  48. package/src/components/header/PrimaryMenu.tsx +100 -100
  49. package/src/components/header/SecondaryMenu.tsx +23 -23
  50. package/src/components/header/SecondaryMenuAlt.tsx +40 -40
  51. package/src/components/header/SiteHeader.tsx +207 -207
  52. package/src/components/header/SupportDrawer.tsx +111 -111
  53. package/src/components/header/index.tsx +2 -2
  54. package/src/components/index.tsx +20 -20
  55. package/src/components/notes/AddNote.tsx +180 -180
  56. package/src/components/notes/Note.tsx +68 -68
  57. package/src/components/notes/Notes.tsx +208 -208
  58. package/src/components/notes/index.ts +3 -3
  59. package/src/components/wrapper/AppList.tsx +19 -19
  60. package/src/components/wrapper/ChurchList.tsx +154 -154
  61. package/src/components/wrapper/NavItem.tsx +47 -47
  62. package/src/components/wrapper/NewPrivateMessage.tsx +253 -253
  63. package/src/components/wrapper/Notifications.tsx +223 -223
  64. package/src/components/wrapper/PrivateMessageDetails.tsx +112 -112
  65. package/src/components/wrapper/PrivateMessages.tsx +576 -576
  66. package/src/components/wrapper/UserMenu.tsx +383 -383
  67. package/src/components/wrapper/index.tsx +8 -8
  68. package/src/helpers/AnalyticsHelper.ts +32 -32
  69. package/src/helpers/AppearanceHelper.ts +73 -73
  70. package/src/helpers/ArrayHelper.ts +87 -87
  71. package/src/helpers/CurrencyHelper.ts +10 -10
  72. package/src/helpers/DateHelper.ts +104 -104
  73. package/src/helpers/ErrorHelper.ts +43 -43
  74. package/src/helpers/EventHelper.ts +49 -49
  75. package/src/helpers/FileHelper.ts +31 -31
  76. package/src/helpers/Locale.ts +457 -457
  77. package/src/helpers/NotificationService.ts +296 -296
  78. package/src/helpers/PersonHelper.ts +62 -62
  79. package/src/helpers/SlugHelper.ts +37 -37
  80. package/src/helpers/SocketHelper.ts +296 -296
  81. package/src/helpers/UniqueIdHelper.ts +36 -36
  82. package/src/helpers/UserHelper.ts +104 -104
  83. package/src/helpers/createEmotionCache.ts +17 -17
  84. package/src/helpers/index.ts +58 -50
  85. package/src/hooks/index.ts +3 -3
  86. package/src/hooks/useMountedState.ts +16 -16
  87. package/src/hooks/useNotifications.ts +93 -93
  88. package/src/index.ts +2 -2
  89. package/src/types/interface-extensions.d.ts +12 -0
  90. package/tsconfig.json +31 -31
  91. package/dist/public/locales/de.json +0 -270
  92. package/dist/public/locales/en.json +0 -277
  93. package/dist/public/locales/es.json +0 -272
  94. package/dist/public/locales/fr.json +0 -270
  95. package/dist/public/locales/hi.json +0 -270
  96. package/dist/public/locales/it.json +0 -270
  97. package/dist/public/locales/ko.json +0 -270
  98. package/dist/public/locales/no.json +0 -270
  99. package/dist/public/locales/pt.json +0 -270
  100. package/dist/public/locales/ru.json +0 -270
  101. package/dist/public/locales/tl.json +0 -270
  102. package/dist/public/locales/zh.json +0 -270
@@ -1,457 +1,457 @@
1
- import i18n from "i18next";
2
- import { initReactI18next } from "react-i18next/initReactI18next";
3
- import LanguageDetector from "i18next-browser-languagedetector";
4
- import Backend from "i18next-chained-backend";
5
-
6
- interface TranslationResources {
7
- [key: string]: {
8
- translation: Record<string, unknown>;
9
- };
10
- }
11
-
12
- interface ExtraLanguageCodes {
13
- [key: string]: string[];
14
- }
15
-
16
- export class Locale {
17
- private static readonly supportedLanguages: string[] = [
18
- "de",
19
- "en",
20
- "es",
21
- "fr",
22
- "hi",
23
- "it",
24
- "ko",
25
- "no",
26
- "pt",
27
- "ru",
28
- "tl",
29
- "zh",
30
- ];
31
- private static readonly extraCodes: ExtraLanguageCodes = { no: ["nb", "nn"] };
32
-
33
- // Hard-coded English fallbacks for when locale files are not available
34
- private static readonly englishFallbacks: Record<string, any> = {
35
- "b1Share": {
36
- "comment": "Comment",
37
- "commentPlaceholder": "Include a comment with your post.",
38
- "contentShared": "Content Shared",
39
- "group": "Group",
40
- "sharingToGroup": "Sharing {} to B1 Group",
41
- "validate": {
42
- "addComment": "Please add a comment.",
43
- "loginFirst": "Please login first.",
44
- "notMember": "You are not currently a member of any groups on B1.",
45
- "selectGroup": "Please select a group."
46
- }
47
- },
48
- "common": {
49
- "add": "Add",
50
- "cancel": "Cancel",
51
- "close": "Close",
52
- "date": "Date",
53
- "delete": "Delete",
54
- "edit": "Edit",
55
- "error": "Error",
56
- "pleaseWait": "Please wait...",
57
- "save": "Save",
58
- "search": "Search",
59
- "submit": "Submit",
60
- "update": "Update"
61
- },
62
- "createPerson": {
63
- "addNewPerson": "Add a New Person",
64
- "firstName": "First Name",
65
- "lastName": "Last Name",
66
- "email": "Email"
67
- },
68
- "donation": {
69
- "bankForm": {
70
- "accountNumber": "Account Number",
71
- "added": "Bank account added. Verify your bank account to make a donation.",
72
- "company": "Company",
73
- "firstDeposit": "First Deposit",
74
- "holderName": "Account holder name is required.",
75
- "individual": "Individual",
76
- "name": "Account Holder Name",
77
- "needVerified": "Bank accounts will need to be verified before making any donations. Your account will receive two small deposits in approximately 1-3 business days. You will need to enter those deposit amounts to finish verifying your account by selecting the verify account link next to your bank account under the payment methods section.",
78
- "routingNumber": "Routing Number",
79
- "secondDeposit": "Second Deposit",
80
- "twoDeposits": "Enter the two deposits you received in your account to finish verifying your bank account.",
81
- "updated": "Bank account updated.",
82
- "verified": "Bank account verified.",
83
- "validate": {
84
- "accountNumber": "Routing and account number are required.",
85
- "holderName": "Account holder name is required."
86
- }
87
- },
88
- "cardForm": {
89
- "addNew": "Add New Card",
90
- "added": "Card added successfully.",
91
- "expirationMonth": "Expiration Month:",
92
- "expirationYear": "Expiration Year:",
93
- "updated": "Card updated successfully."
94
- },
95
- "common": {
96
- "cancel": "Cancel",
97
- "error": "Error"
98
- },
99
- "donationForm": {
100
- "annually": "Annually",
101
- "biWeekly": "Bi-Weekly",
102
- "cancelled": "Recurring donation cancelled.",
103
- "confirmDelete": "Are you sure you wish to delete this recurring donation?",
104
- "cover": "I'll generously add {} to cover the transaction fees so you can keep 100% of my donation.",
105
- "donate": "Donate",
106
- "editRecurring": "Edit Recurring Donation",
107
- "fees": "Transaction fees of {} are applied.",
108
- "frequency": "Frequency",
109
- "fund": "Fund",
110
- "funds": "Funds",
111
- "make": "Make a Donation",
112
- "makeRecurring": "Make a Recurring Donation",
113
- "method": "Method",
114
- "monthly": "Monthly",
115
- "notes": "Notes",
116
- "preview": "Preview Donation",
117
- "quarterly": "Quarterly",
118
- "recurringUpdated": "Recurring donation updated.",
119
- "startDate": "Start Date",
120
- "thankYou": "Thank you for your donation!",
121
- "tooLow": "Donation amount must be greater than $0.50",
122
- "total": "Total Donation Amount",
123
- "validate": {
124
- "amount": "Amount cannot be $0.",
125
- "email": "Please enter your email address.",
126
- "firstName": "Please enter your first name.",
127
- "lastName": "Please enter your last name.",
128
- "validEmail": "Please enter a valid email address."
129
- },
130
- "weekly": "Weekly"
131
- },
132
- "fundDonations": {
133
- "addMore": "Add More",
134
- "amount": "Amount",
135
- "fund": "Fund"
136
- },
137
- "paymentMethods": {
138
- "addBank": "Add Bank Account",
139
- "addCard": "Add Card",
140
- "confirmDelete": "Are you sure you wish to delete this payment method?",
141
- "deleted": "Payment method deleted.",
142
- "noMethod": "No payment methods. Add a payment method to make a donation.",
143
- "verify": "Verify Account"
144
- },
145
- "page": {
146
- "amount": "Amount",
147
- "batch": "Batch",
148
- "date": "Date",
149
- "fund": "Fund",
150
- "method": "Method",
151
- "willAppear": "Donations will appear once a donation has been entered."
152
- },
153
- "preview": {
154
- "date": "Donation Date",
155
- "donate": "Donate",
156
- "every": "Recurring Every",
157
- "fee": "Transaction Fee",
158
- "funds": "Funds",
159
- "method": "Donation Method",
160
- "notes": "Notes",
161
- "startingOn": "Starting On",
162
- "total": "Total",
163
- "type": "Donation Type",
164
- "weekly": "Weekly"
165
- },
166
- "recurring": {
167
- "amount": "Amount",
168
- "every": "Every",
169
- "interval": "Interval",
170
- "notFound": "Payment method not found.",
171
- "paymentMethod": "Payment Method",
172
- "startDate": "Start Date"
173
- }
174
- },
175
- "formSubmissionEdit": {
176
- "confirmDelete": "Are you sure you wish to delete this form data?",
177
- "editForm": "Edit Form",
178
- "isRequired": "is required",
179
- "submit": "Submit"
180
- },
181
- "gallery": {
182
- "aspectRatio": "Aspect Ratio",
183
- "confirmDelete": "Are you sure you wish to delete this image from gallery?",
184
- "freeForm": "Free Form"
185
- },
186
- "iconPicker": {
187
- "iconsAvailable": "icons available",
188
- "matchingResults": "matching results",
189
- "search": "Search icons..."
190
- },
191
- "login": {
192
- "createAccount": "Create an Account",
193
- "email": "Email",
194
- "expiredLink": "The current link is expired.",
195
- "forgot": "Forgot Password",
196
- "goLogin": "Go to Login",
197
- "login": "Login",
198
- "password": "Password",
199
- "register": "Register",
200
- "registerThankYou": "Thank you for registering! Please check your email to verify your account.",
201
- "requestLink": "Request a new reset link",
202
- "reset": "Reset",
203
- "resetInstructions": "Enter your email address to request a password reset.",
204
- "resetPassword": "Reset Password",
205
- "resetSent": "Password reset email sent!",
206
- "setPassword": "Set Password",
207
- "signIn": "Sign In",
208
- "signInTitle": "Please Sign In",
209
- "verifyPassword": "Verify Password",
210
- "validate": {
211
- "email": "Please enter a valid email address.",
212
- "firstName": "Please enter your first name.",
213
- "invalid": "Invalid login. Please check your email or password.",
214
- "lastName": "Please enter your last name.",
215
- "password": "Please enter a password.",
216
- "passwordLength": "Password must be at least 8 characters long.",
217
- "passwordMatch": "Passwords do not match.",
218
- "selectingChurch": "Error in selecting church. Please verify and try again"
219
- },
220
- "welcomeBack": "Welcome back",
221
- "welcomeName": "Welcome back, <b>{}</b>! Please wait while we load your data."
222
- },
223
- "markdownEditor": {
224
- "content": "Content",
225
- "markdownEditor": "Markdown Editor",
226
- "markdownGuide": "Markdown Guide"
227
- },
228
- "month": {
229
- "april": "April",
230
- "august": "August",
231
- "december": "December",
232
- "february": "February",
233
- "january": "January",
234
- "july": "July",
235
- "june": "June",
236
- "march": "March",
237
- "may": "May",
238
- "november": "November",
239
- "october": "October",
240
- "september": "September"
241
- },
242
- "notes": {
243
- "comment": "comment",
244
- "comments": "comments",
245
- "notes": "Notes",
246
- "startConversation": "Start a conversation",
247
- "validate": {
248
- "content": "Please enter a note."
249
- },
250
- "viewAll": "View all"
251
- },
252
- "person": {
253
- "email": "Email",
254
- "firstName": "First Name",
255
- "lastName": "Last Name",
256
- "name": "Name",
257
- "person": "Person",
258
- "years": "years",
259
- "noRec": "Don't have a person record?"
260
- },
261
- "reporting": {
262
- "detailed": "Detailed Report",
263
- "summary": "Summary",
264
- "sampleTemplate": "Sample Word Template",
265
- "downloadOptions": "Download Options",
266
- "noData": "There is no data to display.",
267
- "runReport": "Run Report",
268
- "useFilter": "Use the filter to run the report."
269
- },
270
- "selectChurch": {
271
- "address1": "Address Line 1",
272
- "address2": "Address Line 2",
273
- "another": "Choose another church",
274
- "city": "City",
275
- "confirmRegister": "Are you sure you wish to register a new church?",
276
- "country": "Country",
277
- "name": "Church Name",
278
- "noMatches": "No matches found.",
279
- "register": "Register a New Church",
280
- "selectChurch": "Select a Church",
281
- "state": "State / Province",
282
- "zip": "Zip / Postal Code",
283
- "validate": {
284
- "address": "Address cannot be blank.",
285
- "city": "City cannot be blank.",
286
- "country": "Country cannot be blank.",
287
- "name": "Church name cannot be blank.",
288
- "state": "State/Province cannot be blank.",
289
- "zip": "Zip/Postal code cannot be blank."
290
- }
291
- },
292
- "stockPhotos": {
293
- "photoBy": "Photo by:",
294
- "providedBy": "Stock photos provided by"
295
- },
296
- "support": {
297
- "documentation": "Documentation",
298
- "discussions": "Support Forum"
299
- },
300
- "wrapper": {
301
- "chatWith": "Chat with",
302
- "deleteChurch": "Delete",
303
- "logout": "Logout",
304
- "messages": "Messages",
305
- "newPrivateMessage": "New Private Message",
306
- "notifications": "Notifications",
307
- "privateMessage": "Private Message",
308
- "privateConversation": "Private Conversation",
309
- "profile": "Profile",
310
- "searchForPerson": "Search for a person",
311
- "support": "Support",
312
- "sureRemoveChurch": "Are you sure you wish to delete this church? You no longer will be a member of {}.",
313
- "switchApp": "Switch App",
314
- "switchChurch": "Switch Church"
315
- }
316
- };
317
-
318
- static init = async (backends: string[]): Promise<void> => {
319
- const resources: TranslationResources = {};
320
- let langs = ["en"];
321
-
322
- if (typeof navigator !== "undefined") {
323
- const browserLang = navigator.language.split("-")[0];
324
- const mappedLang
325
- = Object.keys(this.extraCodes).find((code) =>
326
- this.extraCodes[code].includes(browserLang),
327
- ) || browserLang;
328
- const notSupported = this.supportedLanguages.indexOf(mappedLang) === -1;
329
- langs = mappedLang === "en" || notSupported ? ["en"] : ["en", mappedLang];
330
- }
331
-
332
- // Load translations for each language
333
- for (const lang of langs) {
334
- resources[lang] = { translation: {} };
335
- for (const backend of backends) {
336
- const url = backend.replace("{{lng}}", lang);
337
- try {
338
- const data = await fetch(url).then((response) => response.json());
339
- resources[lang].translation = this.deepMerge(
340
- resources[lang].translation,
341
- data,
342
- );
343
- } catch (error) {
344
- // If fetching fails, we'll rely on the hard-coded fallbacks
345
- console.warn(`Failed to load translations from ${url}:`, error);
346
- }
347
- }
348
- }
349
-
350
- // Initialize i18n
351
- await i18n
352
- .use(Backend)
353
- .use(LanguageDetector)
354
- .use(initReactI18next)
355
- .init({
356
- resources,
357
- fallbackLng: "en",
358
- debug: false,
359
- interpolation: {
360
- escapeValue: false,
361
- },
362
- detection: {
363
- order: ["navigator"],
364
- caches: ["localStorage"],
365
- },
366
- });
367
- };
368
-
369
- private static deepMerge(
370
- target: Record<string, unknown>,
371
- source: Record<string, unknown>,
372
- ): Record<string, unknown> {
373
- for (const key in source) {
374
- if (this.isObject(source[key])) {
375
- if (!target[key]) Object.assign(target, { [key]: {} });
376
- this.deepMerge(
377
- target[key] as Record<string, unknown>,
378
- source[key] as Record<string, unknown>,
379
- );
380
- } else Object.assign(target, { [key]: source[key] });
381
- }
382
- return target;
383
- }
384
-
385
- private static isObject(obj: unknown): boolean {
386
- return obj !== null && typeof obj === "object" && !Array.isArray(obj);
387
- }
388
-
389
- // Helper method to get value from nested object using dot notation
390
- private static getNestedValue(obj: Record<string, any>, path: string): any {
391
- return path.split('.').reduce((current, key) => {
392
- return current && current[key] !== undefined ? current[key] : undefined;
393
- }, obj);
394
- }
395
-
396
- // New helper method that uses i18n with hard-coded English fallback
397
- static t(key: string, options?: Record<string, unknown>): string {
398
- try {
399
- // Check if i18n is initialized and has the key
400
- if (i18n && i18n.isInitialized && i18n.exists(key)) {
401
- const translation = i18n.t(key, options);
402
- // If translation is not the same as the key, return it
403
- if (translation !== key) {
404
- return translation;
405
- }
406
- }
407
- } catch (error) {
408
- // If i18n fails, fall through to hard-coded fallback
409
- console.warn(`i18n translation failed for key "${key}":`, error);
410
- }
411
-
412
- // Fallback to hard-coded English translations
413
- const fallbackValue = this.getNestedValue(this.englishFallbacks, key);
414
- if (fallbackValue !== undefined) {
415
- // Handle simple string interpolation for options
416
- if (typeof fallbackValue === 'string' && options) {
417
- let result = fallbackValue;
418
- Object.keys(options).forEach(optionKey => {
419
- const placeholder = `{{${optionKey}}}`;
420
- if (result.includes(placeholder)) {
421
- result = result.replace(new RegExp(placeholder, 'g'), String(options[optionKey]));
422
- }
423
- // Also handle {} placeholder for backward compatibility
424
- if (result.includes('{}')) {
425
- result = result.replace('{}', String(options[optionKey]));
426
- }
427
- });
428
- return result;
429
- }
430
- return String(fallbackValue);
431
- }
432
-
433
- // If no fallback found, return the key itself
434
- return key;
435
- }
436
-
437
- // Keep the old method for backward compatibility
438
- static label(key: string, fallback?: string): string {
439
- const translation = this.t(key);
440
- // If translation equals the key, it means no translation was found
441
- if (translation === key && fallback) {
442
- return fallback;
443
- }
444
- return translation;
445
- }
446
-
447
- // Helper method to check if i18n is initialized
448
- static isInitialized(): boolean {
449
- return i18n && i18n.isInitialized;
450
- }
451
-
452
- // Method to set up basic fallback-only mode (no i18n)
453
- static setupFallbackMode(): void {
454
- // This method can be called if apps want to use only the hard-coded fallbacks
455
- // without initializing the full i18n system
456
- }
457
- }
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next/initReactI18next";
3
+ import LanguageDetector from "i18next-browser-languagedetector";
4
+ import Backend from "i18next-chained-backend";
5
+
6
+ interface TranslationResources {
7
+ [key: string]: {
8
+ translation: Record<string, unknown>;
9
+ };
10
+ }
11
+
12
+ interface ExtraLanguageCodes {
13
+ [key: string]: string[];
14
+ }
15
+
16
+ export class Locale {
17
+ private static readonly supportedLanguages: string[] = [
18
+ "de",
19
+ "en",
20
+ "es",
21
+ "fr",
22
+ "hi",
23
+ "it",
24
+ "ko",
25
+ "no",
26
+ "pt",
27
+ "ru",
28
+ "tl",
29
+ "zh",
30
+ ];
31
+ private static readonly extraCodes: ExtraLanguageCodes = { no: ["nb", "nn"] };
32
+
33
+ // Hard-coded English fallbacks for when locale files are not available
34
+ private static readonly englishFallbacks: Record<string, any> = {
35
+ "b1Share": {
36
+ "comment": "Comment",
37
+ "commentPlaceholder": "Include a comment with your post.",
38
+ "contentShared": "Content Shared",
39
+ "group": "Group",
40
+ "sharingToGroup": "Sharing {} to B1 Group",
41
+ "validate": {
42
+ "addComment": "Please add a comment.",
43
+ "loginFirst": "Please login first.",
44
+ "notMember": "You are not currently a member of any groups on B1.",
45
+ "selectGroup": "Please select a group."
46
+ }
47
+ },
48
+ "common": {
49
+ "add": "Add",
50
+ "cancel": "Cancel",
51
+ "close": "Close",
52
+ "date": "Date",
53
+ "delete": "Delete",
54
+ "edit": "Edit",
55
+ "error": "Error",
56
+ "pleaseWait": "Please wait...",
57
+ "save": "Save",
58
+ "search": "Search",
59
+ "submit": "Submit",
60
+ "update": "Update"
61
+ },
62
+ "createPerson": {
63
+ "addNewPerson": "Add a New Person",
64
+ "firstName": "First Name",
65
+ "lastName": "Last Name",
66
+ "email": "Email"
67
+ },
68
+ "donation": {
69
+ "bankForm": {
70
+ "accountNumber": "Account Number",
71
+ "added": "Bank account added. Verify your bank account to make a donation.",
72
+ "company": "Company",
73
+ "firstDeposit": "First Deposit",
74
+ "holderName": "Account holder name is required.",
75
+ "individual": "Individual",
76
+ "name": "Account Holder Name",
77
+ "needVerified": "Bank accounts will need to be verified before making any donations. Your account will receive two small deposits in approximately 1-3 business days. You will need to enter those deposit amounts to finish verifying your account by selecting the verify account link next to your bank account under the payment methods section.",
78
+ "routingNumber": "Routing Number",
79
+ "secondDeposit": "Second Deposit",
80
+ "twoDeposits": "Enter the two deposits you received in your account to finish verifying your bank account.",
81
+ "updated": "Bank account updated.",
82
+ "verified": "Bank account verified.",
83
+ "validate": {
84
+ "accountNumber": "Routing and account number are required.",
85
+ "holderName": "Account holder name is required."
86
+ }
87
+ },
88
+ "cardForm": {
89
+ "addNew": "Add New Card",
90
+ "added": "Card added successfully.",
91
+ "expirationMonth": "Expiration Month:",
92
+ "expirationYear": "Expiration Year:",
93
+ "updated": "Card updated successfully."
94
+ },
95
+ "common": {
96
+ "cancel": "Cancel",
97
+ "error": "Error"
98
+ },
99
+ "donationForm": {
100
+ "annually": "Annually",
101
+ "biWeekly": "Bi-Weekly",
102
+ "cancelled": "Recurring donation cancelled.",
103
+ "confirmDelete": "Are you sure you wish to delete this recurring donation?",
104
+ "cover": "I'll generously add {} to cover the transaction fees so you can keep 100% of my donation.",
105
+ "donate": "Donate",
106
+ "editRecurring": "Edit Recurring Donation",
107
+ "fees": "Transaction fees of {} are applied.",
108
+ "frequency": "Frequency",
109
+ "fund": "Fund",
110
+ "funds": "Funds",
111
+ "make": "Make a Donation",
112
+ "makeRecurring": "Make a Recurring Donation",
113
+ "method": "Method",
114
+ "monthly": "Monthly",
115
+ "notes": "Notes",
116
+ "preview": "Preview Donation",
117
+ "quarterly": "Quarterly",
118
+ "recurringUpdated": "Recurring donation updated.",
119
+ "startDate": "Start Date",
120
+ "thankYou": "Thank you for your donation!",
121
+ "tooLow": "Donation amount must be greater than $0.50",
122
+ "total": "Total Donation Amount",
123
+ "validate": {
124
+ "amount": "Amount cannot be $0.",
125
+ "email": "Please enter your email address.",
126
+ "firstName": "Please enter your first name.",
127
+ "lastName": "Please enter your last name.",
128
+ "validEmail": "Please enter a valid email address."
129
+ },
130
+ "weekly": "Weekly"
131
+ },
132
+ "fundDonations": {
133
+ "addMore": "Add More",
134
+ "amount": "Amount",
135
+ "fund": "Fund"
136
+ },
137
+ "paymentMethods": {
138
+ "addBank": "Add Bank Account",
139
+ "addCard": "Add Card",
140
+ "confirmDelete": "Are you sure you wish to delete this payment method?",
141
+ "deleted": "Payment method deleted.",
142
+ "noMethod": "No payment methods. Add a payment method to make a donation.",
143
+ "verify": "Verify Account"
144
+ },
145
+ "page": {
146
+ "amount": "Amount",
147
+ "batch": "Batch",
148
+ "date": "Date",
149
+ "fund": "Fund",
150
+ "method": "Method",
151
+ "willAppear": "Donations will appear once a donation has been entered."
152
+ },
153
+ "preview": {
154
+ "date": "Donation Date",
155
+ "donate": "Donate",
156
+ "every": "Recurring Every",
157
+ "fee": "Transaction Fee",
158
+ "funds": "Funds",
159
+ "method": "Donation Method",
160
+ "notes": "Notes",
161
+ "startingOn": "Starting On",
162
+ "total": "Total",
163
+ "type": "Donation Type",
164
+ "weekly": "Weekly"
165
+ },
166
+ "recurring": {
167
+ "amount": "Amount",
168
+ "every": "Every",
169
+ "interval": "Interval",
170
+ "notFound": "Payment method not found.",
171
+ "paymentMethod": "Payment Method",
172
+ "startDate": "Start Date"
173
+ }
174
+ },
175
+ "formSubmissionEdit": {
176
+ "confirmDelete": "Are you sure you wish to delete this form data?",
177
+ "editForm": "Edit Form",
178
+ "isRequired": "is required",
179
+ "submit": "Submit"
180
+ },
181
+ "gallery": {
182
+ "aspectRatio": "Aspect Ratio",
183
+ "confirmDelete": "Are you sure you wish to delete this image from gallery?",
184
+ "freeForm": "Free Form"
185
+ },
186
+ "iconPicker": {
187
+ "iconsAvailable": "icons available",
188
+ "matchingResults": "matching results",
189
+ "search": "Search icons..."
190
+ },
191
+ "login": {
192
+ "createAccount": "Create an Account",
193
+ "email": "Email",
194
+ "expiredLink": "The current link is expired.",
195
+ "forgot": "Forgot Password",
196
+ "goLogin": "Go to Login",
197
+ "login": "Login",
198
+ "password": "Password",
199
+ "register": "Register",
200
+ "registerThankYou": "Thank you for registering! Please check your email to verify your account.",
201
+ "requestLink": "Request a new reset link",
202
+ "reset": "Reset",
203
+ "resetInstructions": "Enter your email address to request a password reset.",
204
+ "resetPassword": "Reset Password",
205
+ "resetSent": "Password reset email sent!",
206
+ "setPassword": "Set Password",
207
+ "signIn": "Sign In",
208
+ "signInTitle": "Please Sign In",
209
+ "verifyPassword": "Verify Password",
210
+ "validate": {
211
+ "email": "Please enter a valid email address.",
212
+ "firstName": "Please enter your first name.",
213
+ "invalid": "Invalid login. Please check your email or password.",
214
+ "lastName": "Please enter your last name.",
215
+ "password": "Please enter a password.",
216
+ "passwordLength": "Password must be at least 8 characters long.",
217
+ "passwordMatch": "Passwords do not match.",
218
+ "selectingChurch": "Error in selecting church. Please verify and try again"
219
+ },
220
+ "welcomeBack": "Welcome back",
221
+ "welcomeName": "Welcome back, <b>{}</b>! Please wait while we load your data."
222
+ },
223
+ "markdownEditor": {
224
+ "content": "Content",
225
+ "markdownEditor": "Markdown Editor",
226
+ "markdownGuide": "Markdown Guide"
227
+ },
228
+ "month": {
229
+ "april": "April",
230
+ "august": "August",
231
+ "december": "December",
232
+ "february": "February",
233
+ "january": "January",
234
+ "july": "July",
235
+ "june": "June",
236
+ "march": "March",
237
+ "may": "May",
238
+ "november": "November",
239
+ "october": "October",
240
+ "september": "September"
241
+ },
242
+ "notes": {
243
+ "comment": "comment",
244
+ "comments": "comments",
245
+ "notes": "Notes",
246
+ "startConversation": "Start a conversation",
247
+ "validate": {
248
+ "content": "Please enter a note."
249
+ },
250
+ "viewAll": "View all"
251
+ },
252
+ "person": {
253
+ "email": "Email",
254
+ "firstName": "First Name",
255
+ "lastName": "Last Name",
256
+ "name": "Name",
257
+ "person": "Person",
258
+ "years": "years",
259
+ "noRec": "Don't have a person record?"
260
+ },
261
+ "reporting": {
262
+ "detailed": "Detailed Report",
263
+ "summary": "Summary",
264
+ "sampleTemplate": "Sample Word Template",
265
+ "downloadOptions": "Download Options",
266
+ "noData": "There is no data to display.",
267
+ "runReport": "Run Report",
268
+ "useFilter": "Use the filter to run the report."
269
+ },
270
+ "selectChurch": {
271
+ "address1": "Address Line 1",
272
+ "address2": "Address Line 2",
273
+ "another": "Choose another church",
274
+ "city": "City",
275
+ "confirmRegister": "Are you sure you wish to register a new church?",
276
+ "country": "Country",
277
+ "name": "Church Name",
278
+ "noMatches": "No matches found.",
279
+ "register": "Register a New Church",
280
+ "selectChurch": "Select a Church",
281
+ "state": "State / Province",
282
+ "zip": "Zip / Postal Code",
283
+ "validate": {
284
+ "address": "Address cannot be blank.",
285
+ "city": "City cannot be blank.",
286
+ "country": "Country cannot be blank.",
287
+ "name": "Church name cannot be blank.",
288
+ "state": "State/Province cannot be blank.",
289
+ "zip": "Zip/Postal code cannot be blank."
290
+ }
291
+ },
292
+ "stockPhotos": {
293
+ "photoBy": "Photo by:",
294
+ "providedBy": "Stock photos provided by"
295
+ },
296
+ "support": {
297
+ "documentation": "Documentation",
298
+ "discussions": "Support Forum"
299
+ },
300
+ "wrapper": {
301
+ "chatWith": "Chat with",
302
+ "deleteChurch": "Delete",
303
+ "logout": "Logout",
304
+ "messages": "Messages",
305
+ "newPrivateMessage": "New Private Message",
306
+ "notifications": "Notifications",
307
+ "privateMessage": "Private Message",
308
+ "privateConversation": "Private Conversation",
309
+ "profile": "Profile",
310
+ "searchForPerson": "Search for a person",
311
+ "support": "Support",
312
+ "sureRemoveChurch": "Are you sure you wish to delete this church? You no longer will be a member of {}.",
313
+ "switchApp": "Switch App",
314
+ "switchChurch": "Switch Church"
315
+ }
316
+ };
317
+
318
+ static init = async (backends: string[]): Promise<void> => {
319
+ const resources: TranslationResources = {};
320
+ let langs = ["en"];
321
+
322
+ if (typeof navigator !== "undefined") {
323
+ const browserLang = navigator.language.split("-")[0];
324
+ const mappedLang
325
+ = Object.keys(this.extraCodes).find((code) =>
326
+ this.extraCodes[code].includes(browserLang),
327
+ ) || browserLang;
328
+ const notSupported = this.supportedLanguages.indexOf(mappedLang) === -1;
329
+ langs = mappedLang === "en" || notSupported ? ["en"] : ["en", mappedLang];
330
+ }
331
+
332
+ // Load translations for each language
333
+ for (const lang of langs) {
334
+ resources[lang] = { translation: {} };
335
+ for (const backend of backends) {
336
+ const url = backend.replace("{{lng}}", lang);
337
+ try {
338
+ const data = await fetch(url).then((response) => response.json());
339
+ resources[lang].translation = this.deepMerge(
340
+ resources[lang].translation,
341
+ data,
342
+ );
343
+ } catch (error) {
344
+ // If fetching fails, we'll rely on the hard-coded fallbacks
345
+ console.warn(`Failed to load translations from ${url}:`, error);
346
+ }
347
+ }
348
+ }
349
+
350
+ // Initialize i18n
351
+ await i18n
352
+ .use(Backend)
353
+ .use(LanguageDetector)
354
+ .use(initReactI18next)
355
+ .init({
356
+ resources,
357
+ fallbackLng: "en",
358
+ debug: false,
359
+ interpolation: {
360
+ escapeValue: false,
361
+ },
362
+ detection: {
363
+ order: ["navigator"],
364
+ caches: ["localStorage"],
365
+ },
366
+ });
367
+ };
368
+
369
+ private static deepMerge(
370
+ target: Record<string, unknown>,
371
+ source: Record<string, unknown>,
372
+ ): Record<string, unknown> {
373
+ for (const key in source) {
374
+ if (this.isObject(source[key])) {
375
+ if (!target[key]) Object.assign(target, { [key]: {} });
376
+ this.deepMerge(
377
+ target[key] as Record<string, unknown>,
378
+ source[key] as Record<string, unknown>,
379
+ );
380
+ } else Object.assign(target, { [key]: source[key] });
381
+ }
382
+ return target;
383
+ }
384
+
385
+ private static isObject(obj: unknown): boolean {
386
+ return obj !== null && typeof obj === "object" && !Array.isArray(obj);
387
+ }
388
+
389
+ // Helper method to get value from nested object using dot notation
390
+ private static getNestedValue(obj: Record<string, any>, path: string): any {
391
+ return path.split('.').reduce((current, key) => {
392
+ return current && current[key] !== undefined ? current[key] : undefined;
393
+ }, obj);
394
+ }
395
+
396
+ // New helper method that uses i18n with hard-coded English fallback
397
+ static t(key: string, options?: Record<string, unknown>): string {
398
+ try {
399
+ // Check if i18n is initialized and has the key
400
+ if (i18n && i18n.isInitialized && i18n.exists(key)) {
401
+ const translation = i18n.t(key, options);
402
+ // If translation is not the same as the key, return it
403
+ if (translation !== key) {
404
+ return translation;
405
+ }
406
+ }
407
+ } catch (error) {
408
+ // If i18n fails, fall through to hard-coded fallback
409
+ console.warn(`i18n translation failed for key "${key}":`, error);
410
+ }
411
+
412
+ // Fallback to hard-coded English translations
413
+ const fallbackValue = this.getNestedValue(this.englishFallbacks, key);
414
+ if (fallbackValue !== undefined) {
415
+ // Handle simple string interpolation for options
416
+ if (typeof fallbackValue === 'string' && options) {
417
+ let result = fallbackValue;
418
+ Object.keys(options).forEach(optionKey => {
419
+ const placeholder = `{{${optionKey}}}`;
420
+ if (result.includes(placeholder)) {
421
+ result = result.replace(new RegExp(placeholder, 'g'), String(options[optionKey]));
422
+ }
423
+ // Also handle {} placeholder for backward compatibility
424
+ if (result.includes('{}')) {
425
+ result = result.replace('{}', String(options[optionKey]));
426
+ }
427
+ });
428
+ return result;
429
+ }
430
+ return String(fallbackValue);
431
+ }
432
+
433
+ // If no fallback found, return the key itself
434
+ return key;
435
+ }
436
+
437
+ // Keep the old method for backward compatibility
438
+ static label(key: string, fallback?: string): string {
439
+ const translation = this.t(key);
440
+ // If translation equals the key, it means no translation was found
441
+ if (translation === key && fallback) {
442
+ return fallback;
443
+ }
444
+ return translation;
445
+ }
446
+
447
+ // Helper method to check if i18n is initialized
448
+ static isInitialized(): boolean {
449
+ return i18n && i18n.isInitialized;
450
+ }
451
+
452
+ // Method to set up basic fallback-only mode (no i18n)
453
+ static setupFallbackMode(): void {
454
+ // This method can be called if apps want to use only the hard-coded fallbacks
455
+ // without initializing the full i18n system
456
+ }
457
+ }