@shellapps/experience-react 1.1.0 → 1.2.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.
- package/dist/index.d.mts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +85 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +86 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -48,12 +48,21 @@ interface TranslationResult {
|
|
|
48
48
|
locales: Array<{
|
|
49
49
|
code: string;
|
|
50
50
|
name: string;
|
|
51
|
+
progress?: number;
|
|
51
52
|
}>;
|
|
53
|
+
isLoading: boolean;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
|
-
* Translation hook
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Translation hook for the Experience Platform.
|
|
57
|
+
*
|
|
58
|
+
* Fetches translations from the Experience API, caches in memory + localStorage,
|
|
59
|
+
* and provides `t()` for interpolation and `setLocale()` for switching.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const { t, locale, setLocale, locales } = useTranslation();
|
|
64
|
+
* return <h1>{t('greeting', { name: 'Alex' })}</h1>;
|
|
65
|
+
* ```
|
|
57
66
|
*/
|
|
58
67
|
declare function useTranslation(): TranslationResult;
|
|
59
68
|
|
package/dist/index.d.ts
CHANGED
|
@@ -48,12 +48,21 @@ interface TranslationResult {
|
|
|
48
48
|
locales: Array<{
|
|
49
49
|
code: string;
|
|
50
50
|
name: string;
|
|
51
|
+
progress?: number;
|
|
51
52
|
}>;
|
|
53
|
+
isLoading: boolean;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
|
-
* Translation hook
|
|
55
|
-
*
|
|
56
|
-
*
|
|
56
|
+
* Translation hook for the Experience Platform.
|
|
57
|
+
*
|
|
58
|
+
* Fetches translations from the Experience API, caches in memory + localStorage,
|
|
59
|
+
* and provides `t()` for interpolation and `setLocale()` for switching.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* const { t, locale, setLocale, locales } = useTranslation();
|
|
64
|
+
* return <h1>{t('greeting', { name: 'Alex' })}</h1>;
|
|
65
|
+
* ```
|
|
57
66
|
*/
|
|
58
67
|
declare function useTranslation(): TranslationResult;
|
|
59
68
|
|
package/dist/index.js
CHANGED
|
@@ -163,23 +163,101 @@ function useFlag(flagName, defaultValue) {
|
|
|
163
163
|
const experience = useExperience();
|
|
164
164
|
return experience.getFlag(flagName, defaultValue);
|
|
165
165
|
}
|
|
166
|
+
var translationCache = {};
|
|
167
|
+
var fetchPromises = /* @__PURE__ */ new Map();
|
|
166
168
|
function useTranslation() {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
169
|
+
const experience = useExperience();
|
|
170
|
+
const config = experience.config || {};
|
|
171
|
+
const appId = config.appId || "";
|
|
172
|
+
const apiBaseUrl = config.apiUrl || config.baseUrl || "https://experience-api.shellapps.com";
|
|
173
|
+
const [locale, setLocaleState] = (0, import_react4.useState)(() => {
|
|
174
|
+
if (typeof window === "undefined") return "en";
|
|
175
|
+
const saved = localStorage.getItem(`exp_locale_${appId}`);
|
|
176
|
+
if (saved) return saved;
|
|
177
|
+
return navigator.language?.split("-")[0] || "en";
|
|
178
|
+
});
|
|
179
|
+
const [translations, setTranslations] = (0, import_react4.useState)(() => {
|
|
180
|
+
if (translationCache[locale]) return translationCache[locale];
|
|
181
|
+
if (typeof window !== "undefined") {
|
|
182
|
+
try {
|
|
183
|
+
const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);
|
|
184
|
+
if (cached) {
|
|
185
|
+
const parsed = JSON.parse(cached);
|
|
186
|
+
translationCache[locale] = parsed;
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {};
|
|
193
|
+
});
|
|
194
|
+
const [locales, setLocales] = (0, import_react4.useState)([]);
|
|
195
|
+
const [isLoading, setIsLoading] = (0, import_react4.useState)(false);
|
|
196
|
+
const fetchTranslations = (0, import_react4.useCallback)(async (loc) => {
|
|
197
|
+
if (!appId) return {};
|
|
198
|
+
const cacheKey = `${appId}_${loc}`;
|
|
199
|
+
const existing = fetchPromises.get(cacheKey);
|
|
200
|
+
if (existing) return existing;
|
|
201
|
+
const promise = (async () => {
|
|
202
|
+
try {
|
|
203
|
+
const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);
|
|
204
|
+
if (!res.ok) return {};
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
translationCache[loc] = data;
|
|
207
|
+
if (typeof window !== "undefined") {
|
|
208
|
+
try {
|
|
209
|
+
localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return data;
|
|
214
|
+
} catch {
|
|
215
|
+
return {};
|
|
216
|
+
} finally {
|
|
217
|
+
fetchPromises.delete(cacheKey);
|
|
218
|
+
}
|
|
219
|
+
})();
|
|
220
|
+
fetchPromises.set(cacheKey, promise);
|
|
221
|
+
return promise;
|
|
222
|
+
}, [appId, apiBaseUrl]);
|
|
223
|
+
(0, import_react4.useEffect)(() => {
|
|
224
|
+
if (!appId) return;
|
|
225
|
+
fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`).then((res) => res.ok ? res.json() : { locales: [] }).then((data) => setLocales(data.locales || [])).catch(() => {
|
|
226
|
+
});
|
|
227
|
+
}, [appId, apiBaseUrl]);
|
|
228
|
+
(0, import_react4.useEffect)(() => {
|
|
229
|
+
if (!appId) return;
|
|
230
|
+
if (translationCache[locale]) {
|
|
231
|
+
setTranslations(translationCache[locale]);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
setIsLoading(true);
|
|
235
|
+
fetchTranslations(locale).then((data) => {
|
|
236
|
+
setTranslations(data);
|
|
237
|
+
setIsLoading(false);
|
|
238
|
+
});
|
|
239
|
+
}, [locale, appId, fetchTranslations]);
|
|
170
240
|
const t = (0, import_react4.useCallback)((key, params) => {
|
|
171
|
-
let result = key;
|
|
241
|
+
let result = translations[key] ?? key;
|
|
242
|
+
if (result.includes("||") && params && "count" in params) {
|
|
243
|
+
const parts = result.split("||");
|
|
244
|
+
const count = Number(params.count);
|
|
245
|
+
result = count === 1 ? parts[0] ?? result : parts[1] ?? result;
|
|
246
|
+
}
|
|
172
247
|
if (params) {
|
|
173
248
|
for (const [k, v] of Object.entries(params)) {
|
|
174
249
|
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
|
175
250
|
}
|
|
176
251
|
}
|
|
177
252
|
return result;
|
|
178
|
-
}, []);
|
|
253
|
+
}, [translations]);
|
|
179
254
|
const setLocale = (0, import_react4.useCallback)((newLocale) => {
|
|
180
255
|
setLocaleState(newLocale);
|
|
181
|
-
|
|
182
|
-
|
|
256
|
+
if (typeof window !== "undefined") {
|
|
257
|
+
localStorage.setItem(`exp_locale_${appId}`, newLocale);
|
|
258
|
+
}
|
|
259
|
+
}, [appId]);
|
|
260
|
+
return { t, locale, setLocale, locales, isLoading };
|
|
183
261
|
}
|
|
184
262
|
// Annotate the CommonJS export names for ESM import in node:
|
|
185
263
|
0 && (module.exports = {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/ExperienceProvider.tsx","../src/context.ts","../src/ErrorBoundary.tsx","../src/hooks.ts"],"sourcesContent":["export { ExperienceProvider } from './ExperienceProvider';\nexport type { ExperienceProviderProps } from './ExperienceProvider';\nexport { ErrorBoundary } from './ErrorBoundary';\nexport { useExperience, useTrack, useFlag, useTranslation } from './hooks';\nexport { ExperienceContext } from './context';\n","import React, { useEffect, useRef } from 'react';\nimport { Experience } from '@shellapps/experience';\nimport type { ExperienceConfig } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\nexport interface ExperienceProviderProps {\n appId: string;\n apiKey: string;\n profileId?: string;\n options?: Partial<Omit<ExperienceConfig, 'appId' | 'apiKey'>>;\n children: React.ReactNode;\n}\n\nexport function ExperienceProvider({ appId, apiKey, profileId, options, children }: ExperienceProviderProps) {\n const instanceRef = useRef<Experience | null>(null);\n\n if (!instanceRef.current) {\n const instance = Experience.init({ appId, apiKey, ...options });\n if (profileId) instance.identify(profileId);\n instanceRef.current = instance;\n }\n\n useEffect(() => {\n if (profileId && instanceRef.current) {\n instanceRef.current.identify(profileId);\n }\n }, [profileId]);\n\n // data-t auto-tracking\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n let el = e.target as HTMLElement | null;\n while (el) {\n const tid = el.getAttribute?.('data-t');\n if (tid) {\n instanceRef.current?.track('element_click', { elementTid: tid });\n break;\n }\n el = el.parentElement;\n }\n };\n document.addEventListener('click', handler, true);\n return () => document.removeEventListener('click', handler, true);\n }, []);\n\n useEffect(() => {\n return () => {\n instanceRef.current?.shutdown();\n };\n }, []);\n\n return (\n <ExperienceContext.Provider value={instanceRef.current}>\n {children}\n </ExperienceContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { Experience } from '@shellapps/experience';\n\nexport const ExperienceContext = createContext<Experience | null>(null);\n","import React from 'react';\nimport type { Experience } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\ninterface ErrorBoundaryProps {\n fallback?: React.ReactNode;\n showCommentForm?: boolean;\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n children: React.ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n comment: string;\n submitted: boolean;\n}\n\nexport class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {\n static contextType = ExperienceContext;\n declare context: Experience | null;\n\n state: ErrorBoundaryState = {\n hasError: false,\n error: null,\n comment: '',\n submitted: false,\n };\n\n static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.context?.captureError(error, {\n extra: { componentStack: errorInfo.componentStack || '' },\n severity: 'fatal' as any,\n });\n this.props.onError?.(error, errorInfo);\n }\n\n private handleSubmitComment = () => {\n if (this.state.comment.trim() && this.state.error) {\n this.context?.captureMessage(\n `User feedback: ${this.state.comment}`,\n 'info',\n );\n this.setState({ submitted: true });\n }\n };\n\n render() {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div style={{ padding: 20, textAlign: 'center' }}>\n <h2>Something went wrong</h2>\n <p>{this.state.error?.message}</p>\n {this.props.showCommentForm && !this.state.submitted && (\n <div>\n <textarea\n placeholder=\"Tell us what happened...\"\n value={this.state.comment}\n onChange={(e) => this.setState({ comment: e.target.value })}\n style={{ width: '100%', minHeight: 80, marginTop: 10 }}\n />\n <button onClick={this.handleSubmitComment} style={{ marginTop: 8 }}>\n Submit\n </button>\n </div>\n )}\n {this.state.submitted && <p>Thank you for your feedback!</p>}\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { useContext, useState, useCallback, useMemo } from 'react';\nimport { ExperienceContext } from './context';\n\nexport function useExperience() {\n const ctx = useContext(ExperienceContext);\n if (!ctx) throw new Error('useExperience must be used within ExperienceProvider');\n return ctx;\n}\n\nexport function useTrack() {\n const experience = useExperience();\n return useMemo(() => ({\n track: (eventName: string, metadata?: Record<string, string>) => experience.track(eventName, metadata),\n trackPageView: () => experience.trackPageView(),\n }), [experience]);\n}\n\nexport function useFlag<T>(flagName: string, defaultValue: T): T {\n const experience = useExperience();\n return experience.getFlag(flagName, defaultValue);\n}\n\ninterface TranslationResult {\n t: (key: string, params?: Record<string, string | number>) => string;\n locale: string;\n setLocale: (locale: string) => void;\n locales: Array<{ code: string; name: string }>;\n}\n\n/**\n * Translation hook — stub implementation for EXP-5.\n * Currently returns the key with interpolation support.\n * Will fetch from the Experience API when translations are implemented.\n */\nexport function useTranslation(): TranslationResult {\n const [locale, setLocaleState] = useState(\n typeof navigator !== 'undefined' ? navigator.language?.split('-')[0] || 'en' : 'en'\n );\n\n const t = useCallback((key: string, params?: Record<string, string | number>): string => {\n let result = key;\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v));\n }\n }\n return result;\n }, []);\n\n const setLocale = useCallback((newLocale: string) => {\n setLocaleState(newLocale);\n }, []);\n\n return { t, locale, setLocale, locales: [] };\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","React","import_react"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/ExperienceProvider.tsx","../src/context.ts","../src/ErrorBoundary.tsx","../src/hooks.ts"],"sourcesContent":["export { ExperienceProvider } from './ExperienceProvider';\nexport type { ExperienceProviderProps } from './ExperienceProvider';\nexport { ErrorBoundary } from './ErrorBoundary';\nexport { useExperience, useTrack, useFlag, useTranslation } from './hooks';\nexport { ExperienceContext } from './context';\n","import React, { useEffect, useRef } from 'react';\nimport { Experience } from '@shellapps/experience';\nimport type { ExperienceConfig } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\nexport interface ExperienceProviderProps {\n appId: string;\n apiKey: string;\n profileId?: string;\n options?: Partial<Omit<ExperienceConfig, 'appId' | 'apiKey'>>;\n children: React.ReactNode;\n}\n\nexport function ExperienceProvider({ appId, apiKey, profileId, options, children }: ExperienceProviderProps) {\n const instanceRef = useRef<Experience | null>(null);\n\n if (!instanceRef.current) {\n const instance = Experience.init({ appId, apiKey, ...options });\n if (profileId) instance.identify(profileId);\n instanceRef.current = instance;\n }\n\n useEffect(() => {\n if (profileId && instanceRef.current) {\n instanceRef.current.identify(profileId);\n }\n }, [profileId]);\n\n // data-t auto-tracking\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n let el = e.target as HTMLElement | null;\n while (el) {\n const tid = el.getAttribute?.('data-t');\n if (tid) {\n instanceRef.current?.track('element_click', { elementTid: tid });\n break;\n }\n el = el.parentElement;\n }\n };\n document.addEventListener('click', handler, true);\n return () => document.removeEventListener('click', handler, true);\n }, []);\n\n useEffect(() => {\n return () => {\n instanceRef.current?.shutdown();\n };\n }, []);\n\n return (\n <ExperienceContext.Provider value={instanceRef.current}>\n {children}\n </ExperienceContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { Experience } from '@shellapps/experience';\n\nexport const ExperienceContext = createContext<Experience | null>(null);\n","import React from 'react';\nimport type { Experience } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\ninterface ErrorBoundaryProps {\n fallback?: React.ReactNode;\n showCommentForm?: boolean;\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n children: React.ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n comment: string;\n submitted: boolean;\n}\n\nexport class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {\n static contextType = ExperienceContext;\n declare context: Experience | null;\n\n state: ErrorBoundaryState = {\n hasError: false,\n error: null,\n comment: '',\n submitted: false,\n };\n\n static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.context?.captureError(error, {\n extra: { componentStack: errorInfo.componentStack || '' },\n severity: 'fatal' as any,\n });\n this.props.onError?.(error, errorInfo);\n }\n\n private handleSubmitComment = () => {\n if (this.state.comment.trim() && this.state.error) {\n this.context?.captureMessage(\n `User feedback: ${this.state.comment}`,\n 'info',\n );\n this.setState({ submitted: true });\n }\n };\n\n render() {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div style={{ padding: 20, textAlign: 'center' }}>\n <h2>Something went wrong</h2>\n <p>{this.state.error?.message}</p>\n {this.props.showCommentForm && !this.state.submitted && (\n <div>\n <textarea\n placeholder=\"Tell us what happened...\"\n value={this.state.comment}\n onChange={(e) => this.setState({ comment: e.target.value })}\n style={{ width: '100%', minHeight: 80, marginTop: 10 }}\n />\n <button onClick={this.handleSubmitComment} style={{ marginTop: 8 }}>\n Submit\n </button>\n </div>\n )}\n {this.state.submitted && <p>Thank you for your feedback!</p>}\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { useContext, useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { ExperienceContext } from './context';\n\nexport function useExperience() {\n const ctx = useContext(ExperienceContext);\n if (!ctx) throw new Error('useExperience must be used within ExperienceProvider');\n return ctx;\n}\n\nexport function useTrack() {\n const experience = useExperience();\n return useMemo(() => ({\n track: (eventName: string, metadata?: Record<string, string>) => experience.track(eventName, metadata),\n trackPageView: () => experience.trackPageView(),\n }), [experience]);\n}\n\nexport function useFlag<T>(flagName: string, defaultValue: T): T {\n const experience = useExperience();\n return experience.getFlag(flagName, defaultValue);\n}\n\n// ─── Translation Types ──────────────────────────────────────────\n\ninterface TranslationResult {\n t: (key: string, params?: Record<string, string | number>) => string;\n locale: string;\n setLocale: (locale: string) => void;\n locales: Array<{ code: string; name: string; progress?: number }>;\n isLoading: boolean;\n}\n\ninterface TranslationCache {\n [locale: string]: Record<string, string>;\n}\n\n// Module-level cache shared across hook instances\nconst translationCache: TranslationCache = {};\nconst fetchPromises = new Map<string, Promise<Record<string, string>>>();\n\n/**\n * Translation hook for the Experience Platform.\n *\n * Fetches translations from the Experience API, caches in memory + localStorage,\n * and provides `t()` for interpolation and `setLocale()` for switching.\n *\n * Usage:\n * ```tsx\n * const { t, locale, setLocale, locales } = useTranslation();\n * return <h1>{t('greeting', { name: 'Alex' })}</h1>;\n * ```\n */\nexport function useTranslation(): TranslationResult {\n const experience = useExperience();\n const config = (experience as any).config || {};\n const appId: string = config.appId || '';\n const apiBaseUrl: string = config.apiUrl || config.baseUrl || 'https://experience-api.shellapps.com';\n\n const [locale, setLocaleState] = useState(() => {\n if (typeof window === 'undefined') return 'en';\n // Check localStorage for persisted locale preference\n const saved = localStorage.getItem(`exp_locale_${appId}`);\n if (saved) return saved;\n return navigator.language?.split('-')[0] || 'en';\n });\n\n const [translations, setTranslations] = useState<Record<string, string>>(() => {\n // Try memory cache first\n if (translationCache[locale]) return translationCache[locale]!;\n // Try localStorage\n if (typeof window !== 'undefined') {\n try {\n const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);\n if (cached) {\n const parsed = JSON.parse(cached);\n translationCache[locale] = parsed;\n return parsed;\n }\n } catch { /* ignore */ }\n }\n return {};\n });\n\n const [locales, setLocales] = useState<Array<{ code: string; name: string; progress?: number }>>([]);\n const [isLoading, setIsLoading] = useState(false);\n\n // Fetch translations for a locale\n const fetchTranslations = useCallback(async (loc: string): Promise<Record<string, string>> => {\n if (!appId) return {};\n\n // Deduplicate in-flight requests\n const cacheKey = `${appId}_${loc}`;\n const existing = fetchPromises.get(cacheKey);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);\n if (!res.ok) return {};\n const data = await res.json();\n // Cache in memory and localStorage\n translationCache[loc] = data;\n if (typeof window !== 'undefined') {\n try {\n localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));\n } catch { /* quota exceeded, ignore */ }\n }\n return data as Record<string, string>;\n } catch {\n return {};\n } finally {\n fetchPromises.delete(cacheKey);\n }\n })();\n\n fetchPromises.set(cacheKey, promise);\n return promise;\n }, [appId, apiBaseUrl]);\n\n // Fetch available locales\n useEffect(() => {\n if (!appId) return;\n fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`)\n .then(res => res.ok ? res.json() : { locales: [] })\n .then(data => setLocales(data.locales || []))\n .catch(() => {});\n }, [appId, apiBaseUrl]);\n\n // Fetch translations when locale changes\n useEffect(() => {\n if (!appId) return;\n\n // Use cached if available\n if (translationCache[locale]) {\n setTranslations(translationCache[locale]!);\n return;\n }\n\n setIsLoading(true);\n fetchTranslations(locale).then(data => {\n setTranslations(data);\n setIsLoading(false);\n });\n }, [locale, appId, fetchTranslations]);\n\n // t() function with interpolation and plural support\n const t = useCallback((key: string, params?: Record<string, string | number>): string => {\n let result = translations[key] ?? key;\n\n // Handle plurals: \"singular||plural\" with count param\n if (result.includes('||') && params && 'count' in params) {\n const parts = result.split('||');\n const count = Number(params.count);\n result = count === 1 ? (parts[0] ?? result) : (parts[1] ?? result);\n }\n\n // Handle interpolation: {{variable}}\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v));\n }\n }\n\n return result;\n }, [translations]);\n\n // setLocale with persistence\n const setLocale = useCallback((newLocale: string) => {\n setLocaleState(newLocale);\n if (typeof window !== 'undefined') {\n localStorage.setItem(`exp_locale_${appId}`, newLocale);\n }\n }, [appId]);\n\n return { t, locale, setLocale, locales, isLoading };\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime","React","import_react"]}
|
package/dist/index.mjs
CHANGED
|
@@ -104,7 +104,7 @@ var ErrorBoundary = class extends React2.Component {
|
|
|
104
104
|
ErrorBoundary.contextType = ExperienceContext;
|
|
105
105
|
|
|
106
106
|
// src/hooks.ts
|
|
107
|
-
import { useContext, useState, useCallback, useMemo } from "react";
|
|
107
|
+
import { useContext, useState, useCallback, useMemo, useEffect as useEffect2 } from "react";
|
|
108
108
|
function useExperience() {
|
|
109
109
|
const ctx = useContext(ExperienceContext);
|
|
110
110
|
if (!ctx) throw new Error("useExperience must be used within ExperienceProvider");
|
|
@@ -121,23 +121,101 @@ function useFlag(flagName, defaultValue) {
|
|
|
121
121
|
const experience = useExperience();
|
|
122
122
|
return experience.getFlag(flagName, defaultValue);
|
|
123
123
|
}
|
|
124
|
+
var translationCache = {};
|
|
125
|
+
var fetchPromises = /* @__PURE__ */ new Map();
|
|
124
126
|
function useTranslation() {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const experience = useExperience();
|
|
128
|
+
const config = experience.config || {};
|
|
129
|
+
const appId = config.appId || "";
|
|
130
|
+
const apiBaseUrl = config.apiUrl || config.baseUrl || "https://experience-api.shellapps.com";
|
|
131
|
+
const [locale, setLocaleState] = useState(() => {
|
|
132
|
+
if (typeof window === "undefined") return "en";
|
|
133
|
+
const saved = localStorage.getItem(`exp_locale_${appId}`);
|
|
134
|
+
if (saved) return saved;
|
|
135
|
+
return navigator.language?.split("-")[0] || "en";
|
|
136
|
+
});
|
|
137
|
+
const [translations, setTranslations] = useState(() => {
|
|
138
|
+
if (translationCache[locale]) return translationCache[locale];
|
|
139
|
+
if (typeof window !== "undefined") {
|
|
140
|
+
try {
|
|
141
|
+
const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);
|
|
142
|
+
if (cached) {
|
|
143
|
+
const parsed = JSON.parse(cached);
|
|
144
|
+
translationCache[locale] = parsed;
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {};
|
|
151
|
+
});
|
|
152
|
+
const [locales, setLocales] = useState([]);
|
|
153
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
154
|
+
const fetchTranslations = useCallback(async (loc) => {
|
|
155
|
+
if (!appId) return {};
|
|
156
|
+
const cacheKey = `${appId}_${loc}`;
|
|
157
|
+
const existing = fetchPromises.get(cacheKey);
|
|
158
|
+
if (existing) return existing;
|
|
159
|
+
const promise = (async () => {
|
|
160
|
+
try {
|
|
161
|
+
const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);
|
|
162
|
+
if (!res.ok) return {};
|
|
163
|
+
const data = await res.json();
|
|
164
|
+
translationCache[loc] = data;
|
|
165
|
+
if (typeof window !== "undefined") {
|
|
166
|
+
try {
|
|
167
|
+
localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return data;
|
|
172
|
+
} catch {
|
|
173
|
+
return {};
|
|
174
|
+
} finally {
|
|
175
|
+
fetchPromises.delete(cacheKey);
|
|
176
|
+
}
|
|
177
|
+
})();
|
|
178
|
+
fetchPromises.set(cacheKey, promise);
|
|
179
|
+
return promise;
|
|
180
|
+
}, [appId, apiBaseUrl]);
|
|
181
|
+
useEffect2(() => {
|
|
182
|
+
if (!appId) return;
|
|
183
|
+
fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`).then((res) => res.ok ? res.json() : { locales: [] }).then((data) => setLocales(data.locales || [])).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
}, [appId, apiBaseUrl]);
|
|
186
|
+
useEffect2(() => {
|
|
187
|
+
if (!appId) return;
|
|
188
|
+
if (translationCache[locale]) {
|
|
189
|
+
setTranslations(translationCache[locale]);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
setIsLoading(true);
|
|
193
|
+
fetchTranslations(locale).then((data) => {
|
|
194
|
+
setTranslations(data);
|
|
195
|
+
setIsLoading(false);
|
|
196
|
+
});
|
|
197
|
+
}, [locale, appId, fetchTranslations]);
|
|
128
198
|
const t = useCallback((key, params) => {
|
|
129
|
-
let result = key;
|
|
199
|
+
let result = translations[key] ?? key;
|
|
200
|
+
if (result.includes("||") && params && "count" in params) {
|
|
201
|
+
const parts = result.split("||");
|
|
202
|
+
const count = Number(params.count);
|
|
203
|
+
result = count === 1 ? parts[0] ?? result : parts[1] ?? result;
|
|
204
|
+
}
|
|
130
205
|
if (params) {
|
|
131
206
|
for (const [k, v] of Object.entries(params)) {
|
|
132
207
|
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
|
133
208
|
}
|
|
134
209
|
}
|
|
135
210
|
return result;
|
|
136
|
-
}, []);
|
|
211
|
+
}, [translations]);
|
|
137
212
|
const setLocale = useCallback((newLocale) => {
|
|
138
213
|
setLocaleState(newLocale);
|
|
139
|
-
|
|
140
|
-
|
|
214
|
+
if (typeof window !== "undefined") {
|
|
215
|
+
localStorage.setItem(`exp_locale_${appId}`, newLocale);
|
|
216
|
+
}
|
|
217
|
+
}, [appId]);
|
|
218
|
+
return { t, locale, setLocale, locales, isLoading };
|
|
141
219
|
}
|
|
142
220
|
export {
|
|
143
221
|
ErrorBoundary,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ExperienceProvider.tsx","../src/context.ts","../src/ErrorBoundary.tsx","../src/hooks.ts"],"sourcesContent":["import React, { useEffect, useRef } from 'react';\nimport { Experience } from '@shellapps/experience';\nimport type { ExperienceConfig } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\nexport interface ExperienceProviderProps {\n appId: string;\n apiKey: string;\n profileId?: string;\n options?: Partial<Omit<ExperienceConfig, 'appId' | 'apiKey'>>;\n children: React.ReactNode;\n}\n\nexport function ExperienceProvider({ appId, apiKey, profileId, options, children }: ExperienceProviderProps) {\n const instanceRef = useRef<Experience | null>(null);\n\n if (!instanceRef.current) {\n const instance = Experience.init({ appId, apiKey, ...options });\n if (profileId) instance.identify(profileId);\n instanceRef.current = instance;\n }\n\n useEffect(() => {\n if (profileId && instanceRef.current) {\n instanceRef.current.identify(profileId);\n }\n }, [profileId]);\n\n // data-t auto-tracking\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n let el = e.target as HTMLElement | null;\n while (el) {\n const tid = el.getAttribute?.('data-t');\n if (tid) {\n instanceRef.current?.track('element_click', { elementTid: tid });\n break;\n }\n el = el.parentElement;\n }\n };\n document.addEventListener('click', handler, true);\n return () => document.removeEventListener('click', handler, true);\n }, []);\n\n useEffect(() => {\n return () => {\n instanceRef.current?.shutdown();\n };\n }, []);\n\n return (\n <ExperienceContext.Provider value={instanceRef.current}>\n {children}\n </ExperienceContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { Experience } from '@shellapps/experience';\n\nexport const ExperienceContext = createContext<Experience | null>(null);\n","import React from 'react';\nimport type { Experience } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\ninterface ErrorBoundaryProps {\n fallback?: React.ReactNode;\n showCommentForm?: boolean;\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n children: React.ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n comment: string;\n submitted: boolean;\n}\n\nexport class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {\n static contextType = ExperienceContext;\n declare context: Experience | null;\n\n state: ErrorBoundaryState = {\n hasError: false,\n error: null,\n comment: '',\n submitted: false,\n };\n\n static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.context?.captureError(error, {\n extra: { componentStack: errorInfo.componentStack || '' },\n severity: 'fatal' as any,\n });\n this.props.onError?.(error, errorInfo);\n }\n\n private handleSubmitComment = () => {\n if (this.state.comment.trim() && this.state.error) {\n this.context?.captureMessage(\n `User feedback: ${this.state.comment}`,\n 'info',\n );\n this.setState({ submitted: true });\n }\n };\n\n render() {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div style={{ padding: 20, textAlign: 'center' }}>\n <h2>Something went wrong</h2>\n <p>{this.state.error?.message}</p>\n {this.props.showCommentForm && !this.state.submitted && (\n <div>\n <textarea\n placeholder=\"Tell us what happened...\"\n value={this.state.comment}\n onChange={(e) => this.setState({ comment: e.target.value })}\n style={{ width: '100%', minHeight: 80, marginTop: 10 }}\n />\n <button onClick={this.handleSubmitComment} style={{ marginTop: 8 }}>\n Submit\n </button>\n </div>\n )}\n {this.state.submitted && <p>Thank you for your feedback!</p>}\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { useContext, useState, useCallback, useMemo } from 'react';\nimport { ExperienceContext } from './context';\n\nexport function useExperience() {\n const ctx = useContext(ExperienceContext);\n if (!ctx) throw new Error('useExperience must be used within ExperienceProvider');\n return ctx;\n}\n\nexport function useTrack() {\n const experience = useExperience();\n return useMemo(() => ({\n track: (eventName: string, metadata?: Record<string, string>) => experience.track(eventName, metadata),\n trackPageView: () => experience.trackPageView(),\n }), [experience]);\n}\n\nexport function useFlag<T>(flagName: string, defaultValue: T): T {\n const experience = useExperience();\n return experience.getFlag(flagName, defaultValue);\n}\n\ninterface TranslationResult {\n t: (key: string, params?: Record<string, string | number>) => string;\n locale: string;\n setLocale: (locale: string) => void;\n locales: Array<{ code: string; name: string }>;\n}\n\n/**\n * Translation hook — stub implementation for EXP-5.\n * Currently returns the key with interpolation support.\n * Will fetch from the Experience API when translations are implemented.\n */\nexport function useTranslation(): TranslationResult {\n const [locale, setLocaleState] = useState(\n typeof navigator !== 'undefined' ? navigator.language?.split('-')[0] || 'en' : 'en'\n );\n\n const t = useCallback((key: string, params?: Record<string, string | number>): string => {\n let result = key;\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v));\n }\n }\n return result;\n }, []);\n\n const setLocale = useCallback((newLocale: string) => {\n setLocaleState(newLocale);\n }, []);\n\n return { t, locale, setLocale, locales: [] };\n}\n"],"mappings":";AAAA,SAAgB,WAAW,cAAc;AACzC,SAAS,kBAAkB;;;ACD3B,SAAS,qBAAqB;AAGvB,IAAM,oBAAoB,cAAiC,IAAI;;;ADiDlE;AAvCG,SAAS,mBAAmB,EAAE,OAAO,QAAQ,WAAW,SAAS,SAAS,GAA4B;AAC3G,QAAM,cAAc,OAA0B,IAAI;AAElD,MAAI,CAAC,YAAY,SAAS;AACxB,UAAM,WAAW,WAAW,KAAK,EAAE,OAAO,QAAQ,GAAG,QAAQ,CAAC;AAC9D,QAAI,UAAW,UAAS,SAAS,SAAS;AAC1C,gBAAY,UAAU;AAAA,EACxB;AAEA,YAAU,MAAM;AACd,QAAI,aAAa,YAAY,SAAS;AACpC,kBAAY,QAAQ,SAAS,SAAS;AAAA,IACxC;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAGd,YAAU,MAAM;AACd,UAAM,UAAU,CAAC,MAAkB;AACjC,UAAI,KAAK,EAAE;AACX,aAAO,IAAI;AACT,cAAM,MAAM,GAAG,eAAe,QAAQ;AACtC,YAAI,KAAK;AACP,sBAAY,SAAS,MAAM,iBAAiB,EAAE,YAAY,IAAI,CAAC;AAC/D;AAAA,QACF;AACA,aAAK,GAAG;AAAA,MACV;AAAA,IACF;AACA,aAAS,iBAAiB,SAAS,SAAS,IAAI;AAChD,WAAO,MAAM,SAAS,oBAAoB,SAAS,SAAS,IAAI;AAAA,EAClE,GAAG,CAAC,CAAC;AAEL,YAAU,MAAM;AACd,WAAO,MAAM;AACX,kBAAY,SAAS,SAAS;AAAA,IAChC;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,SACE,oBAAC,kBAAkB,UAAlB,EAA2B,OAAO,YAAY,SAC5C,UACH;AAEJ;;;AExDA,OAAOA,YAAW;AA2DR,gBAAAC,MAGE,YAHF;AAzCH,IAAM,gBAAN,cAA4BC,OAAM,UAAkD;AAAA,EAApF;AAAA;AAIL,iBAA4B;AAAA,MAC1B,UAAU;AAAA,MACV,OAAO;AAAA,MACP,SAAS;AAAA,MACT,WAAW;AAAA,IACb;AAcA,SAAQ,sBAAsB,MAAM;AAClC,UAAI,KAAK,MAAM,QAAQ,KAAK,KAAK,KAAK,MAAM,OAAO;AACjD,aAAK,SAAS;AAAA,UACZ,kBAAkB,KAAK,MAAM,OAAO;AAAA,UACpC;AAAA,QACF;AACA,aAAK,SAAS,EAAE,WAAW,KAAK,CAAC;AAAA,MACnC;AAAA,IACF;AAAA;AAAA,EApBA,OAAO,yBAAyB,OAA2C;AACzE,WAAO,EAAE,UAAU,MAAM,MAAM;AAAA,EACjC;AAAA,EAEA,kBAAkB,OAAc,WAAkC;AAChE,SAAK,SAAS,aAAa,OAAO;AAAA,MAChC,OAAO,EAAE,gBAAgB,UAAU,kBAAkB,GAAG;AAAA,MACxD,UAAU;AAAA,IACZ,CAAC;AACD,SAAK,MAAM,UAAU,OAAO,SAAS;AAAA,EACvC;AAAA,EAYA,SAAS;AACP,QAAI,KAAK,MAAM,UAAU;AACvB,UAAI,KAAK,MAAM,UAAU;AACvB,eAAO,KAAK,MAAM;AAAA,MACpB;AAEA,aACE,qBAAC,SAAI,OAAO,EAAE,SAAS,IAAI,WAAW,SAAS,GAC7C;AAAA,wBAAAD,KAAC,QAAG,kCAAoB;AAAA,QACxB,gBAAAA,KAAC,OAAG,eAAK,MAAM,OAAO,SAAQ;AAAA,QAC7B,KAAK,MAAM,mBAAmB,CAAC,KAAK,MAAM,aACzC,qBAAC,SACC;AAAA,0BAAAA;AAAA,YAAC;AAAA;AAAA,cACC,aAAY;AAAA,cACZ,OAAO,KAAK,MAAM;AAAA,cAClB,UAAU,CAAC,MAAM,KAAK,SAAS,EAAE,SAAS,EAAE,OAAO,MAAM,CAAC;AAAA,cAC1D,OAAO,EAAE,OAAO,QAAQ,WAAW,IAAI,WAAW,GAAG;AAAA;AAAA,UACvD;AAAA,UACA,gBAAAA,KAAC,YAAO,SAAS,KAAK,qBAAqB,OAAO,EAAE,WAAW,EAAE,GAAG,oBAEpE;AAAA,WACF;AAAA,QAED,KAAK,MAAM,aAAa,gBAAAA,KAAC,OAAE,0CAA4B;AAAA,SAC1D;AAAA,IAEJ;AAEA,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;AA/Da,cACJ,cAAc;;;ACnBvB,SAAS,YAAY,UAAU,aAAa,eAAe;AAGpD,SAAS,gBAAgB;AAC9B,QAAM,MAAM,WAAW,iBAAiB;AACxC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,sDAAsD;AAChF,SAAO;AACT;AAEO,SAAS,WAAW;AACzB,QAAM,aAAa,cAAc;AACjC,SAAO,QAAQ,OAAO;AAAA,IACpB,OAAO,CAAC,WAAmB,aAAsC,WAAW,MAAM,WAAW,QAAQ;AAAA,IACrG,eAAe,MAAM,WAAW,cAAc;AAAA,EAChD,IAAI,CAAC,UAAU,CAAC;AAClB;AAEO,SAAS,QAAW,UAAkB,cAAoB;AAC/D,QAAM,aAAa,cAAc;AACjC,SAAO,WAAW,QAAQ,UAAU,YAAY;AAClD;AAcO,SAAS,iBAAoC;AAClD,QAAM,CAAC,QAAQ,cAAc,IAAI;AAAA,IAC/B,OAAO,cAAc,cAAc,UAAU,UAAU,MAAM,GAAG,EAAE,CAAC,KAAK,OAAO;AAAA,EACjF;AAEA,QAAM,IAAI,YAAY,CAAC,KAAa,WAAqD;AACvF,QAAI,SAAS;AACb,QAAI,QAAQ;AACV,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,iBAAS,OAAO,QAAQ,IAAI,OAAO,SAAS,CAAC,UAAU,GAAG,GAAG,OAAO,CAAC,CAAC;AAAA,MACxE;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY,YAAY,CAAC,cAAsB;AACnD,mBAAe,SAAS;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,GAAG,QAAQ,WAAW,SAAS,CAAC,EAAE;AAC7C;","names":["React","jsx","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/ExperienceProvider.tsx","../src/context.ts","../src/ErrorBoundary.tsx","../src/hooks.ts"],"sourcesContent":["import React, { useEffect, useRef } from 'react';\nimport { Experience } from '@shellapps/experience';\nimport type { ExperienceConfig } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\nexport interface ExperienceProviderProps {\n appId: string;\n apiKey: string;\n profileId?: string;\n options?: Partial<Omit<ExperienceConfig, 'appId' | 'apiKey'>>;\n children: React.ReactNode;\n}\n\nexport function ExperienceProvider({ appId, apiKey, profileId, options, children }: ExperienceProviderProps) {\n const instanceRef = useRef<Experience | null>(null);\n\n if (!instanceRef.current) {\n const instance = Experience.init({ appId, apiKey, ...options });\n if (profileId) instance.identify(profileId);\n instanceRef.current = instance;\n }\n\n useEffect(() => {\n if (profileId && instanceRef.current) {\n instanceRef.current.identify(profileId);\n }\n }, [profileId]);\n\n // data-t auto-tracking\n useEffect(() => {\n const handler = (e: MouseEvent) => {\n let el = e.target as HTMLElement | null;\n while (el) {\n const tid = el.getAttribute?.('data-t');\n if (tid) {\n instanceRef.current?.track('element_click', { elementTid: tid });\n break;\n }\n el = el.parentElement;\n }\n };\n document.addEventListener('click', handler, true);\n return () => document.removeEventListener('click', handler, true);\n }, []);\n\n useEffect(() => {\n return () => {\n instanceRef.current?.shutdown();\n };\n }, []);\n\n return (\n <ExperienceContext.Provider value={instanceRef.current}>\n {children}\n </ExperienceContext.Provider>\n );\n}\n","import { createContext } from 'react';\nimport type { Experience } from '@shellapps/experience';\n\nexport const ExperienceContext = createContext<Experience | null>(null);\n","import React from 'react';\nimport type { Experience } from '@shellapps/experience';\nimport { ExperienceContext } from './context';\n\ninterface ErrorBoundaryProps {\n fallback?: React.ReactNode;\n showCommentForm?: boolean;\n onError?: (error: Error, errorInfo: React.ErrorInfo) => void;\n children: React.ReactNode;\n}\n\ninterface ErrorBoundaryState {\n hasError: boolean;\n error: Error | null;\n comment: string;\n submitted: boolean;\n}\n\nexport class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {\n static contextType = ExperienceContext;\n declare context: Experience | null;\n\n state: ErrorBoundaryState = {\n hasError: false,\n error: null,\n comment: '',\n submitted: false,\n };\n\n static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {\n this.context?.captureError(error, {\n extra: { componentStack: errorInfo.componentStack || '' },\n severity: 'fatal' as any,\n });\n this.props.onError?.(error, errorInfo);\n }\n\n private handleSubmitComment = () => {\n if (this.state.comment.trim() && this.state.error) {\n this.context?.captureMessage(\n `User feedback: ${this.state.comment}`,\n 'info',\n );\n this.setState({ submitted: true });\n }\n };\n\n render() {\n if (this.state.hasError) {\n if (this.props.fallback) {\n return this.props.fallback;\n }\n\n return (\n <div style={{ padding: 20, textAlign: 'center' }}>\n <h2>Something went wrong</h2>\n <p>{this.state.error?.message}</p>\n {this.props.showCommentForm && !this.state.submitted && (\n <div>\n <textarea\n placeholder=\"Tell us what happened...\"\n value={this.state.comment}\n onChange={(e) => this.setState({ comment: e.target.value })}\n style={{ width: '100%', minHeight: 80, marginTop: 10 }}\n />\n <button onClick={this.handleSubmitComment} style={{ marginTop: 8 }}>\n Submit\n </button>\n </div>\n )}\n {this.state.submitted && <p>Thank you for your feedback!</p>}\n </div>\n );\n }\n\n return this.props.children;\n }\n}\n","import { useContext, useState, useCallback, useMemo, useEffect, useRef } from 'react';\nimport { ExperienceContext } from './context';\n\nexport function useExperience() {\n const ctx = useContext(ExperienceContext);\n if (!ctx) throw new Error('useExperience must be used within ExperienceProvider');\n return ctx;\n}\n\nexport function useTrack() {\n const experience = useExperience();\n return useMemo(() => ({\n track: (eventName: string, metadata?: Record<string, string>) => experience.track(eventName, metadata),\n trackPageView: () => experience.trackPageView(),\n }), [experience]);\n}\n\nexport function useFlag<T>(flagName: string, defaultValue: T): T {\n const experience = useExperience();\n return experience.getFlag(flagName, defaultValue);\n}\n\n// ─── Translation Types ──────────────────────────────────────────\n\ninterface TranslationResult {\n t: (key: string, params?: Record<string, string | number>) => string;\n locale: string;\n setLocale: (locale: string) => void;\n locales: Array<{ code: string; name: string; progress?: number }>;\n isLoading: boolean;\n}\n\ninterface TranslationCache {\n [locale: string]: Record<string, string>;\n}\n\n// Module-level cache shared across hook instances\nconst translationCache: TranslationCache = {};\nconst fetchPromises = new Map<string, Promise<Record<string, string>>>();\n\n/**\n * Translation hook for the Experience Platform.\n *\n * Fetches translations from the Experience API, caches in memory + localStorage,\n * and provides `t()` for interpolation and `setLocale()` for switching.\n *\n * Usage:\n * ```tsx\n * const { t, locale, setLocale, locales } = useTranslation();\n * return <h1>{t('greeting', { name: 'Alex' })}</h1>;\n * ```\n */\nexport function useTranslation(): TranslationResult {\n const experience = useExperience();\n const config = (experience as any).config || {};\n const appId: string = config.appId || '';\n const apiBaseUrl: string = config.apiUrl || config.baseUrl || 'https://experience-api.shellapps.com';\n\n const [locale, setLocaleState] = useState(() => {\n if (typeof window === 'undefined') return 'en';\n // Check localStorage for persisted locale preference\n const saved = localStorage.getItem(`exp_locale_${appId}`);\n if (saved) return saved;\n return navigator.language?.split('-')[0] || 'en';\n });\n\n const [translations, setTranslations] = useState<Record<string, string>>(() => {\n // Try memory cache first\n if (translationCache[locale]) return translationCache[locale]!;\n // Try localStorage\n if (typeof window !== 'undefined') {\n try {\n const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);\n if (cached) {\n const parsed = JSON.parse(cached);\n translationCache[locale] = parsed;\n return parsed;\n }\n } catch { /* ignore */ }\n }\n return {};\n });\n\n const [locales, setLocales] = useState<Array<{ code: string; name: string; progress?: number }>>([]);\n const [isLoading, setIsLoading] = useState(false);\n\n // Fetch translations for a locale\n const fetchTranslations = useCallback(async (loc: string): Promise<Record<string, string>> => {\n if (!appId) return {};\n\n // Deduplicate in-flight requests\n const cacheKey = `${appId}_${loc}`;\n const existing = fetchPromises.get(cacheKey);\n if (existing) return existing;\n\n const promise = (async () => {\n try {\n const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);\n if (!res.ok) return {};\n const data = await res.json();\n // Cache in memory and localStorage\n translationCache[loc] = data;\n if (typeof window !== 'undefined') {\n try {\n localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));\n } catch { /* quota exceeded, ignore */ }\n }\n return data as Record<string, string>;\n } catch {\n return {};\n } finally {\n fetchPromises.delete(cacheKey);\n }\n })();\n\n fetchPromises.set(cacheKey, promise);\n return promise;\n }, [appId, apiBaseUrl]);\n\n // Fetch available locales\n useEffect(() => {\n if (!appId) return;\n fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`)\n .then(res => res.ok ? res.json() : { locales: [] })\n .then(data => setLocales(data.locales || []))\n .catch(() => {});\n }, [appId, apiBaseUrl]);\n\n // Fetch translations when locale changes\n useEffect(() => {\n if (!appId) return;\n\n // Use cached if available\n if (translationCache[locale]) {\n setTranslations(translationCache[locale]!);\n return;\n }\n\n setIsLoading(true);\n fetchTranslations(locale).then(data => {\n setTranslations(data);\n setIsLoading(false);\n });\n }, [locale, appId, fetchTranslations]);\n\n // t() function with interpolation and plural support\n const t = useCallback((key: string, params?: Record<string, string | number>): string => {\n let result = translations[key] ?? key;\n\n // Handle plurals: \"singular||plural\" with count param\n if (result.includes('||') && params && 'count' in params) {\n const parts = result.split('||');\n const count = Number(params.count);\n result = count === 1 ? (parts[0] ?? result) : (parts[1] ?? result);\n }\n\n // Handle interpolation: {{variable}}\n if (params) {\n for (const [k, v] of Object.entries(params)) {\n result = result.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, 'g'), String(v));\n }\n }\n\n return result;\n }, [translations]);\n\n // setLocale with persistence\n const setLocale = useCallback((newLocale: string) => {\n setLocaleState(newLocale);\n if (typeof window !== 'undefined') {\n localStorage.setItem(`exp_locale_${appId}`, newLocale);\n }\n }, [appId]);\n\n return { t, locale, setLocale, locales, isLoading };\n}\n"],"mappings":"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","names":["React","jsx","React","useEffect","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellapps/experience-react",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "React SDK for @shellapps/experience",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"url": "git+https://github.com/ShellTechnology/shellapps-js.git",
|
|
44
44
|
"directory": "packages/experience-react"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "71abb6526f26e71e7b991e1bf8ace11c7c4bd3c2"
|
|
47
47
|
}
|