@churchapps/apphelper-login 0.5.0 → 0.5.1

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.
@@ -1,248 +1,248 @@
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
- "common": {
36
- "pleaseWait": "Please wait...",
37
- "search": "Search",
38
- "cancel": "Cancel",
39
- "save": "Save",
40
- "delete": "Delete",
41
- "edit": "Edit",
42
- "add": "Add",
43
- "close": "Close",
44
- "date": "Date",
45
- "error": "Error",
46
- "submit": "Submit",
47
- "update": "Update"
48
- },
49
- "login": {
50
- "createAccount": "Create an Account",
51
- "email": "Email",
52
- "expiredLink": "The current link is expired.",
53
- "forgot": "Forgot Password",
54
- "goLogin": "Go to Login",
55
- "login": "Login",
56
- "password": "Password",
57
- "register": "Register",
58
- "registerThankYou": "Thank you for registering! Please check your email to verify your account.",
59
- "requestLink": "Request a new reset link",
60
- "reset": "Reset",
61
- "resetInstructions": "Enter your email address to request a password reset.",
62
- "resetPassword": "Reset Password",
63
- "resetSent": "Password reset email sent!",
64
- "setPassword": "Set Password",
65
- "signIn": "Sign In",
66
- "signInTitle": "Please Sign In",
67
- "verifyPassword": "Verify Password",
68
- "welcomeName": "Welcome back, <b>{}</b>! Please wait while we load your data.",
69
- "welcomeBack": "Welcome back",
70
- "validate": {
71
- "email": "Please enter a valid email address.",
72
- "firstName": "Please enter your first name.",
73
- "invalid": "Invalid login. Please check your email or password.",
74
- "lastName": "Please enter your last name.",
75
- "password": "Please enter a password.",
76
- "passwordLength": "Password must be at least 8 characters long.",
77
- "passwordMatch": "Passwords do not match.",
78
- "selectingChurch": "Error in selecting church. Please verify and try again"
79
- }
80
- },
81
- "selectChurch": {
82
- "address1": "Address Line 1",
83
- "address2": "Address Line 2",
84
- "another": "Choose another church",
85
- "city": "City",
86
- "confirmRegister": "Are you sure you wish to register a new church?",
87
- "country": "Country",
88
- "name": "Church Name",
89
- "noMatches": "No matches found.",
90
- "register": "Register a New Church",
91
- "selectChurch": "Select a Church",
92
- "state": "State / Province",
93
- "zip": "Zip / Postal Code",
94
- "validate": {
95
- "address": "Address cannot be blank.",
96
- "city": "City cannot be blank.",
97
- "country": "Country cannot be blank.",
98
- "name": "Church name cannot be blank.",
99
- "state": "State/Province cannot be blank.",
100
- "zip": "Zip/Postal code cannot be blank."
101
- }
102
- }
103
- };
104
-
105
- static init = async (backends: string[]): Promise<void> => {
106
- const resources: TranslationResources = {};
107
- let langs = ["en"];
108
-
109
- if (typeof navigator !== "undefined") {
110
- const browserLang = navigator.language.split("-")[0];
111
- const mappedLang
112
- = Object.keys(this.extraCodes).find((code) =>
113
- this.extraCodes[code].includes(browserLang),
114
- ) || browserLang;
115
- const notSupported = this.supportedLanguages.indexOf(mappedLang) === -1;
116
- langs = mappedLang === "en" || notSupported ? ["en"] : ["en", mappedLang];
117
- }
118
-
119
- // Load translations for each language
120
- for (const lang of langs) {
121
- resources[lang] = { translation: {} };
122
- try {
123
- for (const backend of backends) {
124
- const url = backend.replace("{{lng}}", lang);
125
- try {
126
- const response = await fetch(url);
127
- if (response.ok) {
128
- const data = await response.json();
129
- resources[lang].translation = this.deepMerge(
130
- resources[lang].translation,
131
- data,
132
- );
133
- }
134
- } catch (error) {
135
- console.warn(`Failed to load translations from ${url}:`, error);
136
- }
137
- }
138
- } catch (error) {
139
- console.warn(`Failed to load translations for language ${lang}:`, error);
140
- }
141
- }
142
-
143
- // Initialize i18n
144
- try {
145
- await i18n
146
- .use(Backend)
147
- .use(LanguageDetector)
148
- .use(initReactI18next)
149
- .init({
150
- resources,
151
- fallbackLng: "en",
152
- debug: false,
153
- interpolation: {
154
- escapeValue: false,
155
- },
156
- detection: {
157
- order: ["navigator"],
158
- caches: ["localStorage"],
159
- },
160
- });
161
- } catch (error) {
162
- console.warn("Failed to initialize i18n:", error);
163
- }
164
- };
165
-
166
- private static deepMerge(
167
- target: Record<string, unknown>,
168
- source: Record<string, unknown>,
169
- ): Record<string, unknown> {
170
- for (const key in source) {
171
- if (this.isObject(source[key])) {
172
- if (!target[key]) Object.assign(target, { [key]: {} });
173
- this.deepMerge(
174
- target[key] as Record<string, unknown>,
175
- source[key] as Record<string, unknown>,
176
- );
177
- } else Object.assign(target, { [key]: source[key] });
178
- }
179
- return target;
180
- }
181
-
182
- private static isObject(obj: unknown): boolean {
183
- return obj !== null && typeof obj === "object" && !Array.isArray(obj);
184
- }
185
-
186
- // Helper method to get value from nested object using dot notation
187
- private static getNestedValue(obj: Record<string, any>, path: string): any {
188
- return path.split('.').reduce((current, key) => {
189
- return current && current[key] !== undefined ? current[key] : undefined;
190
- }, obj);
191
- }
192
-
193
- // New helper method that uses i18n with hard-coded English fallback
194
- static t(key: string, options?: Record<string, unknown>): string {
195
- try {
196
- // Check if i18n is initialized and has the key
197
- if (i18n && i18n.isInitialized && i18n.exists(key)) {
198
- const translation = i18n.t(key, options);
199
- // If translation is not the same as the key, return it
200
- if (translation !== key) {
201
- return translation;
202
- }
203
- }
204
- } catch (error) {
205
- // If i18n fails, fall through to hard-coded fallback
206
- console.warn(`i18n translation failed for key "${key}":`, error);
207
- }
208
-
209
- // Fallback to hard-coded English translations
210
- const fallbackValue = this.getNestedValue(this.englishFallbacks, key);
211
- if (fallbackValue !== undefined) {
212
- // Handle simple string interpolation for options
213
- if (typeof fallbackValue === 'string' && options) {
214
- let result = fallbackValue;
215
- Object.keys(options).forEach(optionKey => {
216
- const placeholder = `{{${optionKey}}}`;
217
- if (result.includes(placeholder)) {
218
- result = result.replace(new RegExp(placeholder, 'g'), String(options[optionKey]));
219
- }
220
- // Also handle {} placeholder for backward compatibility
221
- if (result.includes('{}')) {
222
- result = result.replace('{}', String(options[optionKey]));
223
- }
224
- });
225
- return result;
226
- }
227
- return String(fallbackValue);
228
- }
229
-
230
- // If no fallback found, return the key itself
231
- return key;
232
- }
233
-
234
- // Keep the old method for backward compatibility
235
- static label(key: string): string {
236
- return this.t(key);
237
- }
238
-
239
- // Helper method to check if i18n is initialized
240
- static isInitialized(): boolean {
241
- return i18n && i18n.isInitialized;
242
- }
243
-
244
- // Method to set up basic fallback-only mode (no i18n)
245
- static initFallbackMode(): void {
246
- console.info("Locale: Running in fallback mode with English labels only");
247
- }
248
- }
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
+ "common": {
36
+ "pleaseWait": "Please wait...",
37
+ "search": "Search",
38
+ "cancel": "Cancel",
39
+ "save": "Save",
40
+ "delete": "Delete",
41
+ "edit": "Edit",
42
+ "add": "Add",
43
+ "close": "Close",
44
+ "date": "Date",
45
+ "error": "Error",
46
+ "submit": "Submit",
47
+ "update": "Update"
48
+ },
49
+ "login": {
50
+ "createAccount": "Create an Account",
51
+ "email": "Email",
52
+ "expiredLink": "The current link is expired.",
53
+ "forgot": "Forgot Password",
54
+ "goLogin": "Go to Login",
55
+ "login": "Login",
56
+ "password": "Password",
57
+ "register": "Register",
58
+ "registerThankYou": "Thank you for registering! Please check your email to verify your account.",
59
+ "requestLink": "Request a new reset link",
60
+ "reset": "Reset",
61
+ "resetInstructions": "Enter your email address to request a password reset.",
62
+ "resetPassword": "Reset Password",
63
+ "resetSent": "Password reset email sent!",
64
+ "setPassword": "Set Password",
65
+ "signIn": "Sign In",
66
+ "signInTitle": "Please Sign In",
67
+ "verifyPassword": "Verify Password",
68
+ "welcomeName": "Welcome back, <b>{}</b>! Please wait while we load your data.",
69
+ "welcomeBack": "Welcome back",
70
+ "validate": {
71
+ "email": "Please enter a valid email address.",
72
+ "firstName": "Please enter your first name.",
73
+ "invalid": "Invalid login. Please check your email or password.",
74
+ "lastName": "Please enter your last name.",
75
+ "password": "Please enter a password.",
76
+ "passwordLength": "Password must be at least 8 characters long.",
77
+ "passwordMatch": "Passwords do not match.",
78
+ "selectingChurch": "Error in selecting church. Please verify and try again"
79
+ }
80
+ },
81
+ "selectChurch": {
82
+ "address1": "Address Line 1",
83
+ "address2": "Address Line 2",
84
+ "another": "Choose another church",
85
+ "city": "City",
86
+ "confirmRegister": "Are you sure you wish to register a new church?",
87
+ "country": "Country",
88
+ "name": "Church Name",
89
+ "noMatches": "No matches found.",
90
+ "register": "Register a New Church",
91
+ "selectChurch": "Select a Church",
92
+ "state": "State / Province",
93
+ "zip": "Zip / Postal Code",
94
+ "validate": {
95
+ "address": "Address cannot be blank.",
96
+ "city": "City cannot be blank.",
97
+ "country": "Country cannot be blank.",
98
+ "name": "Church name cannot be blank.",
99
+ "state": "State/Province cannot be blank.",
100
+ "zip": "Zip/Postal code cannot be blank."
101
+ }
102
+ }
103
+ };
104
+
105
+ static init = async (backends: string[]): Promise<void> => {
106
+ const resources: TranslationResources = {};
107
+ let langs = ["en"];
108
+
109
+ if (typeof navigator !== "undefined") {
110
+ const browserLang = navigator.language.split("-")[0];
111
+ const mappedLang
112
+ = Object.keys(this.extraCodes).find((code) =>
113
+ this.extraCodes[code].includes(browserLang),
114
+ ) || browserLang;
115
+ const notSupported = this.supportedLanguages.indexOf(mappedLang) === -1;
116
+ langs = mappedLang === "en" || notSupported ? ["en"] : ["en", mappedLang];
117
+ }
118
+
119
+ // Load translations for each language
120
+ for (const lang of langs) {
121
+ resources[lang] = { translation: {} };
122
+ try {
123
+ for (const backend of backends) {
124
+ const url = backend.replace("{{lng}}", lang);
125
+ try {
126
+ const response = await fetch(url);
127
+ if (response.ok) {
128
+ const data = await response.json();
129
+ resources[lang].translation = this.deepMerge(
130
+ resources[lang].translation,
131
+ data,
132
+ );
133
+ }
134
+ } catch (error) {
135
+ console.warn(`Failed to load translations from ${url}:`, error);
136
+ }
137
+ }
138
+ } catch (error) {
139
+ console.warn(`Failed to load translations for language ${lang}:`, error);
140
+ }
141
+ }
142
+
143
+ // Initialize i18n
144
+ try {
145
+ await i18n
146
+ .use(Backend)
147
+ .use(LanguageDetector)
148
+ .use(initReactI18next)
149
+ .init({
150
+ resources,
151
+ fallbackLng: "en",
152
+ debug: false,
153
+ interpolation: {
154
+ escapeValue: false,
155
+ },
156
+ detection: {
157
+ order: ["navigator"],
158
+ caches: ["localStorage"],
159
+ },
160
+ });
161
+ } catch (error) {
162
+ console.warn("Failed to initialize i18n:", error);
163
+ }
164
+ };
165
+
166
+ private static deepMerge(
167
+ target: Record<string, unknown>,
168
+ source: Record<string, unknown>,
169
+ ): Record<string, unknown> {
170
+ for (const key in source) {
171
+ if (this.isObject(source[key])) {
172
+ if (!target[key]) Object.assign(target, { [key]: {} });
173
+ this.deepMerge(
174
+ target[key] as Record<string, unknown>,
175
+ source[key] as Record<string, unknown>,
176
+ );
177
+ } else Object.assign(target, { [key]: source[key] });
178
+ }
179
+ return target;
180
+ }
181
+
182
+ private static isObject(obj: unknown): boolean {
183
+ return obj !== null && typeof obj === "object" && !Array.isArray(obj);
184
+ }
185
+
186
+ // Helper method to get value from nested object using dot notation
187
+ private static getNestedValue(obj: Record<string, any>, path: string): any {
188
+ return path.split('.').reduce((current, key) => {
189
+ return current && current[key] !== undefined ? current[key] : undefined;
190
+ }, obj);
191
+ }
192
+
193
+ // New helper method that uses i18n with hard-coded English fallback
194
+ static t(key: string, options?: Record<string, unknown>): string {
195
+ try {
196
+ // Check if i18n is initialized and has the key
197
+ if (i18n && i18n.isInitialized && i18n.exists(key)) {
198
+ const translation = i18n.t(key, options);
199
+ // If translation is not the same as the key, return it
200
+ if (translation !== key) {
201
+ return translation;
202
+ }
203
+ }
204
+ } catch (error) {
205
+ // If i18n fails, fall through to hard-coded fallback
206
+ console.warn(`i18n translation failed for key "${key}":`, error);
207
+ }
208
+
209
+ // Fallback to hard-coded English translations
210
+ const fallbackValue = this.getNestedValue(this.englishFallbacks, key);
211
+ if (fallbackValue !== undefined) {
212
+ // Handle simple string interpolation for options
213
+ if (typeof fallbackValue === 'string' && options) {
214
+ let result = fallbackValue;
215
+ Object.keys(options).forEach(optionKey => {
216
+ const placeholder = `{{${optionKey}}}`;
217
+ if (result.includes(placeholder)) {
218
+ result = result.replace(new RegExp(placeholder, 'g'), String(options[optionKey]));
219
+ }
220
+ // Also handle {} placeholder for backward compatibility
221
+ if (result.includes('{}')) {
222
+ result = result.replace('{}', String(options[optionKey]));
223
+ }
224
+ });
225
+ return result;
226
+ }
227
+ return String(fallbackValue);
228
+ }
229
+
230
+ // If no fallback found, return the key itself
231
+ return key;
232
+ }
233
+
234
+ // Keep the old method for backward compatibility
235
+ static label(key: string): string {
236
+ return this.t(key);
237
+ }
238
+
239
+ // Helper method to check if i18n is initialized
240
+ static isInitialized(): boolean {
241
+ return i18n && i18n.isInitialized;
242
+ }
243
+
244
+ // Method to set up basic fallback-only mode (no i18n)
245
+ static initFallbackMode(): void {
246
+ console.info("Locale: Running in fallback mode with English labels only");
247
+ }
248
+ }
@@ -1,2 +1,2 @@
1
- export { Locale } from "./Locale";
1
+ export { Locale } from "./Locale";
2
2
  export { AnalyticsHelper } from "./AnalyticsHelper";
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
- export { LoginPage } from "./LoginPage";
2
- export { LogoutPage } from "./LogoutPage";
3
- export { Register } from "./components/Register";
4
- export { Login } from "./components/Login";
5
- export { Forgot } from "./components/Forgot";
6
- export { LoginSetPassword } from "./components/LoginSetPassword";
7
- export { SelectChurchModal } from "./components/SelectChurchModal";
8
- export { SelectChurchRegister } from "./components/SelectChurchRegister";
9
- export { SelectChurchSearch } from "./components/SelectChurchSearch";
10
- export { SelectableChurch } from "./components/SelectableChurch";
1
+ export { LoginPage } from "./LoginPage";
2
+ export { LogoutPage } from "./LogoutPage";
3
+ export { Register } from "./components/Register";
4
+ export { Login } from "./components/Login";
5
+ export { Forgot } from "./components/Forgot";
6
+ export { LoginSetPassword } from "./components/LoginSetPassword";
7
+ export { SelectChurchModal } from "./components/SelectChurchModal";
8
+ export { SelectChurchRegister } from "./components/SelectChurchRegister";
9
+ export { SelectChurchSearch } from "./components/SelectChurchSearch";
10
+ export { SelectableChurch } from "./components/SelectableChurch";
11
11
  export { ErrorMessages } from "@churchapps/apphelper";
package/tsconfig.json CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "compilerOptions": {
3
- "module": "ESNext",
4
- "esModuleInterop": true,
5
- "lib": ["ES2021", "DOM"],
6
- "typeRoots": ["node_modules/@types"],
7
- "target": "ES2020",
8
- "noImplicitAny": true,
9
- "moduleResolution": "node",
10
- "sourceMap": true,
11
- "outDir": "dist",
12
- "baseUrl": ".",
13
- "strict": false,
14
- "allowSyntheticDefaultImports": true,
15
- "declaration": true,
16
- "declarationMap": true,
17
- "jsx": "react-jsx",
18
- "skipLibCheck": true,
19
- "forceConsistentCasingInFileNames": true,
20
- "resolveJsonModule": true,
21
- "isolatedModules": true
22
- },
23
- "include": [
24
- "src/**/*"
25
- ],
26
- "exclude": [
27
- "node_modules",
28
- "dist"
29
- ]
1
+ {
2
+ "compilerOptions": {
3
+ "module": "ESNext",
4
+ "esModuleInterop": true,
5
+ "lib": ["ES2021", "DOM"],
6
+ "typeRoots": ["node_modules/@types"],
7
+ "target": "ES2020",
8
+ "noImplicitAny": true,
9
+ "moduleResolution": "node",
10
+ "sourceMap": true,
11
+ "outDir": "dist",
12
+ "baseUrl": ".",
13
+ "strict": false,
14
+ "allowSyntheticDefaultImports": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "jsx": "react-jsx",
18
+ "skipLibCheck": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "resolveJsonModule": true,
21
+ "isolatedModules": true
22
+ },
23
+ "include": [
24
+ "src/**/*"
25
+ ],
26
+ "exclude": [
27
+ "node_modules",
28
+ "dist"
29
+ ]
30
30
  }