@churchapps/apphelper 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/components/Loading.js +36 -36
  2. package/dist/components/notes/Notes.js +27 -27
  3. package/dist/helpers/AnalyticsHelper.d.ts.map +1 -1
  4. package/dist/helpers/AnalyticsHelper.js +21 -6
  5. package/dist/helpers/AnalyticsHelper.js.map +1 -1
  6. package/dist/helpers/UserHelper.d.ts.map +1 -1
  7. package/dist/helpers/UserHelper.js +4 -1
  8. package/dist/helpers/UserHelper.js.map +1 -1
  9. package/dist/public/css/cropper.css +309 -309
  10. package/dist/public/css/styles.css +111 -111
  11. package/dist/public/locales/de.json +270 -0
  12. package/dist/public/locales/en.json +277 -0
  13. package/dist/public/locales/es.json +272 -0
  14. package/dist/public/locales/fr.json +270 -0
  15. package/dist/public/locales/hi.json +270 -0
  16. package/dist/public/locales/it.json +270 -0
  17. package/dist/public/locales/ko.json +270 -0
  18. package/dist/public/locales/no.json +270 -0
  19. package/dist/public/locales/pt.json +270 -0
  20. package/dist/public/locales/ru.json +270 -0
  21. package/dist/public/locales/tl.json +270 -0
  22. package/dist/public/locales/zh.json +270 -0
  23. package/package.json +72 -72
  24. package/public/css/cropper.css +309 -309
  25. package/public/css/styles.css +111 -111
  26. package/public/locales/de.json +269 -269
  27. package/public/locales/en.json +276 -276
  28. package/public/locales/es.json +272 -272
  29. package/public/locales/fr.json +269 -269
  30. package/public/locales/hi.json +269 -269
  31. package/public/locales/it.json +269 -269
  32. package/public/locales/ko.json +269 -269
  33. package/public/locales/no.json +269 -269
  34. package/public/locales/pt.json +269 -269
  35. package/public/locales/ru.json +269 -269
  36. package/public/locales/tl.json +269 -269
  37. package/public/locales/zh.json +269 -269
  38. package/src/components/DisplayBox.tsx +83 -83
  39. package/src/components/ErrorMessages.tsx +28 -28
  40. package/src/components/ExportLink.tsx +81 -81
  41. package/src/components/FloatingSupport.tsx +18 -18
  42. package/src/components/FormCardPayment.tsx +184 -184
  43. package/src/components/FormSubmissionEdit.tsx +168 -168
  44. package/src/components/HelpIcon.tsx +12 -12
  45. package/src/components/ImageEditor.tsx +161 -161
  46. package/src/components/InputBox.tsx +96 -96
  47. package/src/components/Loading.tsx +77 -77
  48. package/src/components/PageHeader.tsx +110 -110
  49. package/src/components/PersonAvatar.tsx +77 -77
  50. package/src/components/QuestionEdit.tsx +99 -99
  51. package/src/components/SmallButton.tsx +42 -42
  52. package/src/components/SupportModal.tsx +32 -32
  53. package/src/components/TabPanel.tsx +28 -28
  54. package/src/components/gallery/GalleryModal.tsx +173 -173
  55. package/src/components/gallery/StockPhotos.tsx +95 -95
  56. package/src/components/gallery/index.ts +1 -1
  57. package/src/components/header/Banner.tsx +11 -11
  58. package/src/components/header/PrimaryMenu.tsx +100 -100
  59. package/src/components/header/SecondaryMenu.tsx +23 -23
  60. package/src/components/header/SecondaryMenuAlt.tsx +40 -40
  61. package/src/components/header/SiteHeader.tsx +207 -207
  62. package/src/components/header/SupportDrawer.tsx +111 -111
  63. package/src/components/header/index.tsx +2 -2
  64. package/src/components/index.tsx +20 -20
  65. package/src/components/notes/AddNote.tsx +180 -180
  66. package/src/components/notes/Note.tsx +68 -68
  67. package/src/components/notes/Notes.tsx +208 -208
  68. package/src/components/notes/index.ts +3 -3
  69. package/src/components/wrapper/AppList.tsx +19 -19
  70. package/src/components/wrapper/ChurchList.tsx +154 -154
  71. package/src/components/wrapper/NavItem.tsx +47 -47
  72. package/src/components/wrapper/NewPrivateMessage.tsx +253 -253
  73. package/src/components/wrapper/Notifications.tsx +223 -223
  74. package/src/components/wrapper/PrivateMessageDetails.tsx +112 -112
  75. package/src/components/wrapper/PrivateMessages.tsx +576 -576
  76. package/src/components/wrapper/UserMenu.tsx +383 -383
  77. package/src/components/wrapper/index.tsx +8 -8
  78. package/src/helpers/AnalyticsHelper.ts +44 -32
  79. package/src/helpers/AppearanceHelper.ts +73 -73
  80. package/src/helpers/ArrayHelper.ts +87 -87
  81. package/src/helpers/CurrencyHelper.ts +10 -10
  82. package/src/helpers/DateHelper.ts +104 -104
  83. package/src/helpers/ErrorHelper.ts +43 -43
  84. package/src/helpers/EventHelper.ts +49 -49
  85. package/src/helpers/FileHelper.ts +31 -31
  86. package/src/helpers/Locale.ts +457 -457
  87. package/src/helpers/NotificationService.ts +296 -296
  88. package/src/helpers/PersonHelper.ts +62 -62
  89. package/src/helpers/SlugHelper.ts +37 -37
  90. package/src/helpers/SocketHelper.ts +296 -296
  91. package/src/helpers/UniqueIdHelper.ts +36 -36
  92. package/src/helpers/UserHelper.ts +107 -104
  93. package/src/helpers/createEmotionCache.ts +17 -17
  94. package/src/helpers/index.ts +58 -58
  95. package/src/hooks/index.ts +3 -3
  96. package/src/hooks/useMountedState.ts +16 -16
  97. package/src/hooks/useNotifications.ts +93 -93
  98. package/src/index.ts +2 -2
  99. package/src/types/interface-extensions.d.ts +11 -11
  100. package/tsconfig.json +31 -31
@@ -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
+ }