@docyrus/i18n 0.0.2 → 0.1.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/README.md +3 -3
- package/dist/core-index.d.ts +24 -4
- package/dist/core-index.js +21 -1
- package/dist/core-index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/{types-BL4CFrnp.d.ts → types-BN0Z4h-R.d.ts} +1 -1
- package/dist/vue/index.d.ts +2 -2
- package/dist/vue/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ pnpm add @docyrus/i18n @docyrus/api-client
|
|
|
46
46
|
|
|
47
47
|
```tsx
|
|
48
48
|
import { DocyrusI18nProvider, useTranslation, useDocyrusI18n } from '@docyrus/i18n';
|
|
49
|
-
import { useDocyrusClient } from '@docyrus/
|
|
49
|
+
import { useDocyrusClient } from '@docyrus/signin';
|
|
50
50
|
|
|
51
51
|
function App() {
|
|
52
52
|
const client = useDocyrusClient();
|
|
@@ -103,7 +103,7 @@ export default async function Layout({ children }: { children: React.ReactNode }
|
|
|
103
103
|
'use client';
|
|
104
104
|
|
|
105
105
|
import { DocyrusI18nProvider, type TranslationDictionary } from '@docyrus/i18n';
|
|
106
|
-
import { useDocyrusClient } from '@docyrus/
|
|
106
|
+
import { useDocyrusClient } from '@docyrus/signin';
|
|
107
107
|
|
|
108
108
|
export function I18nClientProvider({
|
|
109
109
|
children,
|
|
@@ -132,7 +132,7 @@ export function I18nClientProvider({
|
|
|
132
132
|
<script setup lang="ts">
|
|
133
133
|
import { RouterView } from 'vue-router';
|
|
134
134
|
import { DocyrusI18nProvider } from '@docyrus/i18n/vue';
|
|
135
|
-
import { useDocyrusClient } from '@docyrus/
|
|
135
|
+
import { useDocyrusClient } from '@docyrus/signin/vue';
|
|
136
136
|
|
|
137
137
|
const client = useDocyrusClient();
|
|
138
138
|
</script>
|
package/dist/core-index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DocyrusI18nConfig, I as I18nStatus, T as TranslationDictionary } from './types-
|
|
2
|
-
export { a as DocyrusI18nContextValue, F as FallbackStrategy, b as TranslationsResponse } from './types-
|
|
1
|
+
import { D as DocyrusI18nConfig, I as I18nStatus, T as TranslationDictionary } from './types-BN0Z4h-R.js';
|
|
2
|
+
export { a as DocyrusI18nContextValue, F as FallbackStrategy, b as TranslationsResponse } from './types-BN0Z4h-R.js';
|
|
3
3
|
import '@docyrus/api-client';
|
|
4
4
|
|
|
5
5
|
type I18nStateListener = (state: {
|
|
@@ -14,7 +14,7 @@ type I18nStateListener = (state: {
|
|
|
14
14
|
* Manages locale persistence (cookie), translation fetching (API + static merge),
|
|
15
15
|
* and provides the `t()` function for translation lookup with interpolation.
|
|
16
16
|
*
|
|
17
|
-
* Pattern follows AuthManager from @docyrus/
|
|
17
|
+
* Pattern follows AuthManager from @docyrus/signin.
|
|
18
18
|
*/
|
|
19
19
|
declare class I18nManager {
|
|
20
20
|
private status;
|
|
@@ -104,4 +104,24 @@ declare function getLocale(cookieKey: string): string | null;
|
|
|
104
104
|
*/
|
|
105
105
|
declare function setLocale(cookieKey: string, locale: string): void;
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
interface FetchTranslationsSSROptions {
|
|
108
|
+
/** Access token for API authentication. */
|
|
109
|
+
token: string;
|
|
110
|
+
/** API base URL. Required. */
|
|
111
|
+
apiUrl: string;
|
|
112
|
+
/** API endpoint path for translations. Default: 'v1/tenant/translations' */
|
|
113
|
+
translationsEndpoint?: string;
|
|
114
|
+
/** Next.js revalidate interval in seconds. Default: 300 */
|
|
115
|
+
revalidate?: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Fetch translations on the server (SSR / RSC).
|
|
119
|
+
*
|
|
120
|
+
* Designed for Next.js Server Components where React hooks are unavailable.
|
|
121
|
+
* Returns a TranslationDictionary to pass as `staticTranslations` to the provider.
|
|
122
|
+
*
|
|
123
|
+
* Returns `undefined` on failure — the provider will fall back to API fetch on client.
|
|
124
|
+
*/
|
|
125
|
+
declare function fetchTranslationsSSR(options: FetchTranslationsSSROptions): Promise<TranslationDictionary | undefined>;
|
|
126
|
+
|
|
127
|
+
export { DocyrusI18nConfig, type FetchTranslationsSSROptions, I18nManager, type I18nStateListener, I18nStatus, TranslationDictionary, fetchTranslationsSSR, getLocale, interpolate, setLocale };
|
package/dist/core-index.js
CHANGED
|
@@ -241,6 +241,26 @@ var I18nManager = class {
|
|
|
241
241
|
}
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
-
|
|
244
|
+
// src/core/fetch-translations-ssr.ts
|
|
245
|
+
async function fetchTranslationsSSR(options) {
|
|
246
|
+
const {
|
|
247
|
+
token,
|
|
248
|
+
apiUrl,
|
|
249
|
+
translationsEndpoint = "v1/tenant/translations"
|
|
250
|
+
} = options;
|
|
251
|
+
try {
|
|
252
|
+
const url = `${apiUrl.replace(/\/$/, "")}/${translationsEndpoint}`;
|
|
253
|
+
const res = await fetch(url, {
|
|
254
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
255
|
+
});
|
|
256
|
+
if (!res.ok) return void 0;
|
|
257
|
+
const json = await res.json();
|
|
258
|
+
return json.data ?? json;
|
|
259
|
+
} catch {
|
|
260
|
+
return void 0;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export { I18nManager, fetchTranslationsSSR, getLocale, interpolate, setLocale };
|
|
245
265
|
//# sourceMappingURL=core-index.js.map
|
|
246
266
|
//# sourceMappingURL=core-index.js.map
|
package/dist/core-index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/interpolation.ts","../src/core/locale-storage.ts","../src/core/i18n-manager.ts"],"names":[],"mappings":"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","file":"core-index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/app-auth-ui.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/interpolation.ts","../src/core/locale-storage.ts","../src/core/i18n-manager.ts","../src/core/fetch-translations-ssr.ts"],"names":[],"mappings":"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","file":"core-index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/signin.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n","import { type TranslationDictionary, type TranslationsResponse } from '../types';\n\nexport interface FetchTranslationsSSROptions {\n /** Access token for API authentication. */\n token: string;\n /** API base URL. Required. */\n apiUrl: string;\n /** API endpoint path for translations. Default: 'v1/tenant/translations' */\n translationsEndpoint?: string;\n /** Next.js revalidate interval in seconds. Default: 300 */\n revalidate?: number;\n}\n\n/**\n * Fetch translations on the server (SSR / RSC).\n *\n * Designed for Next.js Server Components where React hooks are unavailable.\n * Returns a TranslationDictionary to pass as `staticTranslations` to the provider.\n *\n * Returns `undefined` on failure — the provider will fall back to API fetch on client.\n */\nexport async function fetchTranslationsSSR(\n options: FetchTranslationsSSROptions\n): Promise<TranslationDictionary | undefined> {\n const {\n token,\n apiUrl,\n translationsEndpoint = 'v1/tenant/translations'\n } = options;\n\n try {\n const url = `${apiUrl.replace(/\\/$/, '')}/${translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` }\n });\n\n if (!res.ok) return undefined;\n\n const json = await res.json() as TranslationsResponse;\n\n return json.data ?? (json as unknown as TranslationDictionary);\n } catch {\n return undefined;\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as react from 'react';
|
|
3
3
|
import { ReactNode } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
export { F as FallbackStrategy, I as I18nStatus, T as TranslationDictionary, b as TranslationsResponse } from './types-
|
|
6
|
-
export { I18nManager, I18nStateListener, getLocale, interpolate, setLocale } from './core-index.js';
|
|
4
|
+
import { a as DocyrusI18nContextValue, D as DocyrusI18nConfig } from './types-BN0Z4h-R.js';
|
|
5
|
+
export { F as FallbackStrategy, I as I18nStatus, T as TranslationDictionary, b as TranslationsResponse } from './types-BN0Z4h-R.js';
|
|
6
|
+
export { FetchTranslationsSSROptions, I18nManager, I18nStateListener, fetchTranslationsSSR, getLocale, interpolate, setLocale } from './core-index.js';
|
|
7
7
|
import '@docyrus/api-client';
|
|
8
8
|
|
|
9
9
|
declare const DocyrusI18nContext: react.Context<DocyrusI18nContextValue | null>;
|
package/dist/index.js
CHANGED
|
@@ -335,6 +335,26 @@ function useTranslation() {
|
|
|
335
335
|
return { t: context.t, isLoading: context.isLoading };
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
// src/core/fetch-translations-ssr.ts
|
|
339
|
+
async function fetchTranslationsSSR(options) {
|
|
340
|
+
const {
|
|
341
|
+
token,
|
|
342
|
+
apiUrl,
|
|
343
|
+
translationsEndpoint = "v1/tenant/translations"
|
|
344
|
+
} = options;
|
|
345
|
+
try {
|
|
346
|
+
const url = `${apiUrl.replace(/\/$/, "")}/${translationsEndpoint}`;
|
|
347
|
+
const res = await fetch(url, {
|
|
348
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
349
|
+
});
|
|
350
|
+
if (!res.ok) return void 0;
|
|
351
|
+
const json = await res.json();
|
|
352
|
+
return json.data ?? json;
|
|
353
|
+
} catch {
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export { DocyrusI18nContext, DocyrusI18nProvider, I18nManager, fetchTranslationsSSR, getLocale, interpolate, setLocale, useDocyrusI18n, useTranslation };
|
|
339
359
|
//# sourceMappingURL=index.js.map
|
|
340
360
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/interpolation.ts","../src/core/locale-storage.ts","../src/core/i18n-manager.ts","../src/components/docyrus-i18n-provider.tsx","../src/hooks/use-i18n.ts","../src/hooks/use-translation.ts"],"names":["setLocale","useContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/app-auth-ui.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n","'use client';\n\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode\n} from 'react';\n\nimport { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type DocyrusI18nContextValue,\n type I18nStatus,\n type TranslationDictionary\n} from '../types';\n\nimport { I18nManager } from '../core/i18n-manager';\n\nexport const DocyrusI18nContext = createContext<DocyrusI18nContextValue | null>(null);\n\nexport interface DocyrusI18nProviderProps extends DocyrusI18nConfig {\n children: ReactNode;\n}\n\nexport function DocyrusI18nProvider({\n children,\n ...config\n}: DocyrusI18nProviderProps) {\n const [status, setStatus] = useState<I18nStatus>(\n config.staticTranslations && Object.keys(config.staticTranslations).length > 0 ? 'ready' : 'loading'\n );\n const [locale, setLocaleState] = useState<string | null>(null);\n const [translations, setTranslations] = useState<TranslationDictionary>(\n config.staticTranslations ?? {}\n );\n const [error, setError] = useState<Error | null>(null);\n const managerRef = useRef<I18nManager | null>(null);\n\n useEffect(() => {\n const manager = new I18nManager(config);\n\n managerRef.current = manager;\n setLocaleState(manager.getLocale());\n\n const unsubscribe = manager.subscribe((state) => {\n setStatus(state.status);\n setLocaleState(state.locale);\n setTranslations(state.translations);\n setError(state.error);\n });\n\n manager.initialize();\n\n return () => {\n unsubscribe();\n manager.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // React to client prop changes (null → RestApiClient transition)\n const clientRef = useRef<RestApiClient | null | undefined>(config.client);\n\n useEffect(() => {\n if (config.client !== clientRef.current) {\n clientRef.current = config.client;\n managerRef.current?.updateConfig({ client: config.client });\n }\n }, [config.client]);\n\n const setLocale = useCallback((newLocale: string) => {\n managerRef.current?.setLocale(newLocale);\n }, []);\n\n const refetch = useCallback(async () => {\n await managerRef.current?.fetchTranslations();\n }, []);\n\n const t = useCallback(\n (key: string, params?: Record<string, string | number>) => {\n return managerRef.current?.t(key, params) ?? key;\n },\n // Re-create t when translations change so consumers re-render\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [translations]\n );\n\n const value = useMemo<DocyrusI18nContextValue>(() => ({\n t,\n locale,\n setLocale,\n status,\n isLoading: status === 'loading',\n translations,\n error,\n refetch\n }), [\n t,\n locale,\n setLocale,\n status,\n translations,\n error,\n refetch\n ]);\n\n return (\n <DocyrusI18nContext.Provider value={value}>\n {children}\n </DocyrusI18nContext.Provider>\n );\n}\n","import { useContext } from 'react';\n\nimport { type DocyrusI18nContextValue } from '../types';\n\nimport { DocyrusI18nContext } from '../components/docyrus-i18n-provider';\n\n/**\n * Access the full Docyrus i18n context.\n * Must be used within a `<DocyrusI18nProvider>`.\n *\n * Returns: { t, locale, setLocale, status, isLoading, translations, error, refetch }\n */\nexport function useDocyrusI18n(): DocyrusI18nContextValue {\n const context = useContext(DocyrusI18nContext);\n\n if (!context) {\n throw new Error(\n 'useDocyrusI18n() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return context;\n}\n","import { useContext } from 'react';\n\nimport { DocyrusI18nContext } from '../components/docyrus-i18n-provider';\n\n/**\n * Lightweight hook for translation only.\n * Returns just `t` and `isLoading` — use `useDocyrusI18n()` for full context.\n *\n * Must be used within a `<DocyrusI18nProvider>`.\n */\nexport function useTranslation() {\n const context = useContext(DocyrusI18nContext);\n\n if (!context) {\n throw new Error(\n 'useTranslation() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return { t: context.t, isLoading: context.isLoading };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/core/interpolation.ts","../src/core/locale-storage.ts","../src/core/i18n-manager.ts","../src/components/docyrus-i18n-provider.tsx","../src/hooks/use-i18n.ts","../src/hooks/use-translation.ts","../src/core/fetch-translations-ssr.ts"],"names":["setLocale","useContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/signin.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n","'use client';\n\nimport {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode\n} from 'react';\n\nimport { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type DocyrusI18nContextValue,\n type I18nStatus,\n type TranslationDictionary\n} from '../types';\n\nimport { I18nManager } from '../core/i18n-manager';\n\nexport const DocyrusI18nContext = createContext<DocyrusI18nContextValue | null>(null);\n\nexport interface DocyrusI18nProviderProps extends DocyrusI18nConfig {\n children: ReactNode;\n}\n\nexport function DocyrusI18nProvider({\n children,\n ...config\n}: DocyrusI18nProviderProps) {\n const [status, setStatus] = useState<I18nStatus>(\n config.staticTranslations && Object.keys(config.staticTranslations).length > 0 ? 'ready' : 'loading'\n );\n const [locale, setLocaleState] = useState<string | null>(null);\n const [translations, setTranslations] = useState<TranslationDictionary>(\n config.staticTranslations ?? {}\n );\n const [error, setError] = useState<Error | null>(null);\n const managerRef = useRef<I18nManager | null>(null);\n\n useEffect(() => {\n const manager = new I18nManager(config);\n\n managerRef.current = manager;\n setLocaleState(manager.getLocale());\n\n const unsubscribe = manager.subscribe((state) => {\n setStatus(state.status);\n setLocaleState(state.locale);\n setTranslations(state.translations);\n setError(state.error);\n });\n\n manager.initialize();\n\n return () => {\n unsubscribe();\n manager.destroy();\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // React to client prop changes (null → RestApiClient transition)\n const clientRef = useRef<RestApiClient | null | undefined>(config.client);\n\n useEffect(() => {\n if (config.client !== clientRef.current) {\n clientRef.current = config.client;\n managerRef.current?.updateConfig({ client: config.client });\n }\n }, [config.client]);\n\n const setLocale = useCallback((newLocale: string) => {\n managerRef.current?.setLocale(newLocale);\n }, []);\n\n const refetch = useCallback(async () => {\n await managerRef.current?.fetchTranslations();\n }, []);\n\n const t = useCallback(\n (key: string, params?: Record<string, string | number>) => {\n return managerRef.current?.t(key, params) ?? key;\n },\n // Re-create t when translations change so consumers re-render\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [translations]\n );\n\n const value = useMemo<DocyrusI18nContextValue>(() => ({\n t,\n locale,\n setLocale,\n status,\n isLoading: status === 'loading',\n translations,\n error,\n refetch\n }), [\n t,\n locale,\n setLocale,\n status,\n translations,\n error,\n refetch\n ]);\n\n return (\n <DocyrusI18nContext.Provider value={value}>\n {children}\n </DocyrusI18nContext.Provider>\n );\n}\n","import { useContext } from 'react';\n\nimport { type DocyrusI18nContextValue } from '../types';\n\nimport { DocyrusI18nContext } from '../components/docyrus-i18n-provider';\n\n/**\n * Access the full Docyrus i18n context.\n * Must be used within a `<DocyrusI18nProvider>`.\n *\n * Returns: { t, locale, setLocale, status, isLoading, translations, error, refetch }\n */\nexport function useDocyrusI18n(): DocyrusI18nContextValue {\n const context = useContext(DocyrusI18nContext);\n\n if (!context) {\n throw new Error(\n 'useDocyrusI18n() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return context;\n}\n","import { useContext } from 'react';\n\nimport { DocyrusI18nContext } from '../components/docyrus-i18n-provider';\n\n/**\n * Lightweight hook for translation only.\n * Returns just `t` and `isLoading` — use `useDocyrusI18n()` for full context.\n *\n * Must be used within a `<DocyrusI18nProvider>`.\n */\nexport function useTranslation() {\n const context = useContext(DocyrusI18nContext);\n\n if (!context) {\n throw new Error(\n 'useTranslation() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return { t: context.t, isLoading: context.isLoading };\n}\n","import { type TranslationDictionary, type TranslationsResponse } from '../types';\n\nexport interface FetchTranslationsSSROptions {\n /** Access token for API authentication. */\n token: string;\n /** API base URL. Required. */\n apiUrl: string;\n /** API endpoint path for translations. Default: 'v1/tenant/translations' */\n translationsEndpoint?: string;\n /** Next.js revalidate interval in seconds. Default: 300 */\n revalidate?: number;\n}\n\n/**\n * Fetch translations on the server (SSR / RSC).\n *\n * Designed for Next.js Server Components where React hooks are unavailable.\n * Returns a TranslationDictionary to pass as `staticTranslations` to the provider.\n *\n * Returns `undefined` on failure — the provider will fall back to API fetch on client.\n */\nexport async function fetchTranslationsSSR(\n options: FetchTranslationsSSROptions\n): Promise<TranslationDictionary | undefined> {\n const {\n token,\n apiUrl,\n translationsEndpoint = 'v1/tenant/translations'\n } = options;\n\n try {\n const url = `${apiUrl.replace(/\\/$/, '')}/${translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` }\n });\n\n if (!res.ok) return undefined;\n\n const json = await res.json() as TranslationsResponse;\n\n return json.data ?? (json as unknown as TranslationDictionary);\n } catch {\n return undefined;\n }\n}\n"]}
|
|
@@ -34,7 +34,7 @@ interface DocyrusI18nConfig {
|
|
|
34
34
|
userLanguageEndpoint?: string | false;
|
|
35
35
|
}
|
|
36
36
|
interface DocyrusI18nContextValue {
|
|
37
|
-
/** Translate a key with optional interpolation params. */
|
|
37
|
+
/** Translate a key with optional interpolation params. Supports {{var}}, {var}, and {0} placeholders. */
|
|
38
38
|
t: (key: string, params?: Record<string, string | number>) => string;
|
|
39
39
|
/** Current active locale (null until first setLocale or cookie read). */
|
|
40
40
|
locale: string | null;
|
package/dist/vue/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as vue from 'vue';
|
|
2
2
|
import { PropType } from 'vue';
|
|
3
3
|
import { RestApiClient } from '@docyrus/api-client';
|
|
4
|
-
import {
|
|
5
|
-
export { D as DocyrusI18nConfig } from '../types-
|
|
4
|
+
import { T as TranslationDictionary, F as FallbackStrategy, I as I18nStatus } from '../types-BN0Z4h-R.js';
|
|
5
|
+
export { D as DocyrusI18nConfig } from '../types-BN0Z4h-R.js';
|
|
6
6
|
|
|
7
7
|
interface DocyrusI18nState {
|
|
8
8
|
status: I18nStatus;
|
package/dist/vue/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/interpolation.ts","../../src/core/locale-storage.ts","../../src/core/i18n-manager.ts","../../src/vue/index.ts"],"names":["setLocale"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/app-auth-ui.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n","\nimport { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n ref,\n shallowRef,\n computed,\n defineComponent,\n inject,\n provide,\n watch,\n onMounted,\n onUnmounted,\n type InjectionKey,\n type PropType\n} from 'vue';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary\n} from '../types';\n\nimport { I18nManager } from '../core/i18n-manager';\n\n// ─── Internal reactive i18n state ────────────────────────────────\n\ninterface DocyrusI18nState {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n isLoading: boolean;\n t: (key: string, params?: Record<string, string | number>) => string;\n setLocale: (locale: string) => void;\n refetch: () => Promise<void>;\n}\n\nconst DOCYRUS_I18N_KEY: InjectionKey<DocyrusI18nState> = Symbol('docyrus-i18n');\n\n// ─── Composables ─────────────────────────────────────────────────\n\n/**\n * Vue composable to access full Docyrus i18n state.\n * Must be used within a `<DocyrusI18nProvider>`.\n *\n * Returns: { t, locale, setLocale, status, isLoading, translations, error, refetch }\n */\nexport function useDocyrusI18n(): DocyrusI18nState {\n const context = inject(DOCYRUS_I18N_KEY);\n\n if (!context) {\n throw new Error(\n 'useDocyrusI18n() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return context;\n}\n\n/**\n * Lightweight composable for translation only.\n * Returns just `t` and `isLoading`.\n */\nexport function useTranslation() {\n const context = useDocyrusI18n();\n\n return {\n t: computed(() => context.t),\n isLoading: computed(() => context.isLoading)\n };\n}\n\n// ─── Components ──────────────────────────────────────────────────\n\n/**\n * Vue provider component that wraps your app with Docyrus i18n context.\n *\n * Usage:\n * ```vue\n * <template>\n * <DocyrusI18nProvider :client=\"client\">\n * <App />\n * </DocyrusI18nProvider>\n * </template>\n *\n * <script setup>\n * import { DocyrusI18nProvider } from '@docyrus/i18n/vue';\n * </script>\n * ```\n */\nexport const DocyrusI18nProvider = defineComponent({\n name: 'DocyrusI18nProvider',\n props: {\n client: { type: Object as PropType<RestApiClient | null>, default: null },\n getAccessToken: { type: Function as PropType<() => Promise<string | null> | string | null>, default: undefined },\n apiUrl: { type: String, default: undefined },\n translationsEndpoint: { type: String, default: undefined },\n staticTranslations: { type: Object as PropType<TranslationDictionary>, default: undefined },\n cookieKey: { type: String, default: undefined },\n mergeStrategy: { type: String as PropType<'api-first' | 'static-first'>, default: undefined },\n fallback: { type: [String, Function] as PropType<FallbackStrategy>, default: undefined },\n disableApiFetch: { type: Boolean, default: undefined }\n },\n setup(props, { slots }) {\n const status = ref<I18nStatus>('loading');\n const locale = ref<string | null>(null);\n const translations = shallowRef<TranslationDictionary>({});\n const error = shallowRef<Error | null>(null);\n\n let manager: I18nManager | null = null;\n\n const setLocale = (newLocale: string) => {\n manager?.setLocale(newLocale);\n };\n\n const refetch = async () => {\n await manager?.fetchTranslations();\n };\n\n const t = (key: string, params?: Record<string, string | number>) => {\n return manager?.t(key, params) ?? key;\n };\n\n const i18nState: DocyrusI18nState = {\n get status() { return status.value; },\n get locale() { return locale.value; },\n get translations() { return translations.value; },\n get error() { return error.value; },\n get isLoading() { return status.value === 'loading'; },\n t,\n setLocale,\n refetch\n };\n\n provide(DOCYRUS_I18N_KEY, i18nState);\n\n onMounted(() => {\n const config: DocyrusI18nConfig = {\n client: props.client,\n getAccessToken: props.getAccessToken,\n apiUrl: props.apiUrl,\n translationsEndpoint: props.translationsEndpoint,\n staticTranslations: props.staticTranslations,\n cookieKey: props.cookieKey,\n mergeStrategy: props.mergeStrategy,\n fallback: props.fallback,\n disableApiFetch: props.disableApiFetch\n };\n\n manager = new I18nManager(config);\n locale.value = manager.getLocale();\n\n manager.subscribe((state) => {\n status.value = state.status;\n locale.value = state.locale;\n translations.value = state.translations;\n error.value = state.error;\n });\n\n manager.initialize();\n });\n\n // Watch client prop for null → value transition\n watch(() => props.client, (newClient) => {\n manager?.updateConfig({ client: newClient });\n });\n\n onUnmounted(() => {\n manager?.destroy();\n manager = null;\n });\n\n return () => slots.default?.();\n }\n});\n\n// ─── Type re-exports ─────────────────────────────────────────────\n\nexport type {\n DocyrusI18nState,\n DocyrusI18nConfig,\n I18nStatus,\n TranslationDictionary,\n FallbackStrategy\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/interpolation.ts","../../src/core/locale-storage.ts","../../src/core/i18n-manager.ts","../../src/vue/index.ts"],"names":["setLocale"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Interpolate placeholders in a translation string.\n *\n * Supports three formats (checked in order):\n * 1. {{variable}} — double-brace (static translations convention)\n * 2. {variable} — single-brace named (API convention: \"Talk to {name} AI\")\n * 3. {0}, {1} — single-brace positional (API convention: \"Add New {0}\")\n *\n * Positional placeholders use numeric keys: params = { '0': 'User' }\n *\n * Examples:\n * interpolate('Hello, {{name}}!', { name: 'Ali' }) → 'Hello, Ali!'\n * interpolate('Talk to {name} AI', { name: 'Docyrus' }) → 'Talk to Docyrus AI'\n * interpolate('Add New {0}', { '0': 'User' }) → 'Add New User'\n */\nexport function interpolate(\n template: string,\n params?: Record<string, string | number>\n): string {\n if (!params) return template;\n\n // Match both {{key}} and {key} — double-brace first, then single-brace\n return template.replace(\n /\\{\\{(\\w+)\\}\\}|\\{(\\w+)\\}/g,\n (match, doubleBraceKey: string | undefined, singleBraceKey: string | undefined) => {\n const key = doubleBraceKey ?? singleBraceKey;\n\n if (key === undefined) return match;\n\n const value = params[key];\n\n return value !== undefined ? String(value) : match;\n }\n );\n}\n","const DEFAULT_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds\n\n/**\n * Read the locale from a cookie. SSR-safe.\n * Returns null if the cookie is not set or not in a browser environment.\n */\nexport function getLocale(cookieKey: string): string | null {\n if (typeof document === 'undefined') return null;\n\n const match = document.cookie.match(new RegExp(`(?:^|;\\\\s*)${escapeRegex(cookieKey)}=([^;]*)`));\n\n return match ? decodeURIComponent(match[1]) : null;\n}\n\n/**\n * Write the locale to a cookie. SSR-safe (no-op on server).\n */\nexport function setLocale(cookieKey: string, locale: string): void {\n if (typeof document === 'undefined') return;\n\n document.cookie = `${cookieKey}=${encodeURIComponent(locale)};path=/;max-age=${DEFAULT_MAX_AGE};SameSite=Lax`;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary,\n type TranslationsResponse\n} from '../types';\n\nimport { interpolate } from './interpolation';\nimport { getLocale, setLocale as writeLocaleCookie } from './locale-storage';\n\nexport type I18nStateListener = (state: {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n}) => void;\n\n/**\n * Framework-agnostic translation engine.\n *\n * Manages locale persistence (cookie), translation fetching (API + static merge),\n * and provides the `t()` function for translation lookup with interpolation.\n *\n * Pattern follows AuthManager from @docyrus/signin.\n */\nexport class I18nManager {\n private status: I18nStatus = 'loading';\n private locale: string | null;\n private translations: TranslationDictionary = {};\n private apiTranslations: TranslationDictionary = {};\n private error: Error | null = null;\n private listeners: Set<I18nStateListener> = new Set();\n private abortController: AbortController | null = null;\n private client: RestApiClient | null;\n private getAccessToken: (() => Promise<string | null> | string | null) | undefined;\n private apiUrl: string | undefined;\n private translationsEndpoint: string;\n private staticTranslations: TranslationDictionary;\n private cookieKey: string;\n private mergeStrategy: 'api-first' | 'static-first';\n private fallback: FallbackStrategy;\n private disableApiFetch: boolean;\n private userLanguageEndpoint: string | false;\n constructor(config: DocyrusI18nConfig) {\n this.client = config.client ?? null;\n this.getAccessToken = config.getAccessToken;\n this.apiUrl = config.apiUrl;\n this.translationsEndpoint = config.translationsEndpoint ?? 'v1/tenant/translations';\n this.staticTranslations = config.staticTranslations ?? {};\n this.cookieKey = config.cookieKey ?? 'docyrus-locale';\n this.mergeStrategy = config.mergeStrategy ?? 'api-first';\n this.fallback = config.fallback ?? 'key';\n this.disableApiFetch = config.disableApiFetch ?? false;\n this.userLanguageEndpoint = config.userLanguageEndpoint ?? 'v1/users/me';\n\n // Read locale from cookie (null if not set — API resolves from user profile)\n this.locale = getLocale(this.cookieKey);\n }\n getStatus(): I18nStatus {\n return this.status;\n }\n getLocale(): string | null {\n return this.locale;\n }\n getTranslations(): TranslationDictionary {\n return this.translations;\n }\n getError(): Error | null {\n return this.error;\n }\n subscribe(listener: I18nStateListener): () => void {\n this.listeners.add(listener);\n\n return () => this.listeners.delete(listener);\n }\n private notify(): void {\n for (const listener of this.listeners) {\n listener({\n status: this.status,\n locale: this.locale,\n translations: this.translations,\n error: this.error\n });\n }\n }\n /**\n * Initialize the manager.\n * 1. Apply static translations immediately (no loading flash).\n * 2. Fetch from API if enabled and auth is available.\n */\n async initialize(): Promise<void> {\n // Apply static translations immediately\n if (Object.keys(this.staticTranslations).length > 0) {\n this.translations = { ...this.staticTranslations };\n this.status = 'ready';\n this.notify();\n }\n\n // Fetch from API\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n await this.fetchTranslations();\n } else if (this.status !== 'ready') {\n // No static translations and no API fetch — mark ready with empty translations\n this.status = 'ready';\n this.notify();\n }\n }\n /**\n * Fetch translations from the API.\n * Uses RestApiClient.get() if client is available, otherwise raw fetch with getAccessToken.\n */\n async fetchTranslations(): Promise<void> {\n // Abort any in-flight request\n this.abortController?.abort();\n this.abortController = new AbortController();\n\n try {\n let apiData: TranslationsResponse;\n\n if (this.client) {\n apiData = await this.client.get<TranslationsResponse>(\n this.translationsEndpoint\n );\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) {\n throw new Error('getAccessToken returned null — cannot fetch translations');\n }\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.translationsEndpoint}`;\n const res = await fetch(url, {\n headers: { Authorization: `Bearer ${token}` },\n signal: this.abortController.signal\n });\n\n if (!res.ok) {\n throw new Error(`Translation fetch failed: ${res.status} ${res.statusText}`);\n }\n\n apiData = await res.json() as TranslationsResponse;\n } else {\n // No auth available yet — skip silently\n return;\n }\n\n this.apiTranslations = apiData.data;\n this.mergeTranslations();\n this.error = null;\n this.status = 'ready';\n this.notify();\n } catch (err) {\n if ((err as Error).name === 'AbortError') return;\n\n this.error = err instanceof Error ? err : new Error(String(err));\n\n // If we already have static translations, stay 'ready' with error\n if (Object.keys(this.translations).length > 0) {\n this.notify();\n } else {\n this.status = 'error';\n this.notify();\n }\n }\n }\n /**\n * Translate a key with optional interpolation.\n * Falls back based on the configured strategy.\n */\n t(key: string, params?: Record<string, string | number>): string {\n const value = this.translations[key];\n\n if (value !== undefined) {\n return interpolate(value, params);\n }\n\n // Fallback — still interpolate so {{name}} placeholders resolve\n if (this.fallback === 'key') return interpolate(key, params);\n if (this.fallback === 'empty') return '';\n\n return this.fallback(key);\n }\n /**\n * Change the active locale.\n * Writes to cookie, persists to API (PATCH users/me), and triggers translation refetch.\n */\n async setLocale(locale: string): Promise<void> {\n if (locale === this.locale) return;\n\n this.locale = locale;\n writeLocaleCookie(this.cookieKey, locale);\n this.notify();\n\n // Persist language preference to API + refetch translations in parallel\n const promises: Promise<void>[] = [];\n\n if (this.userLanguageEndpoint !== false && (this.client || this.getAccessToken)) {\n promises.push(this.persistUserLanguage(locale));\n }\n\n if (!this.disableApiFetch && (this.client || this.getAccessToken)) {\n promises.push(this.fetchTranslations());\n }\n\n await Promise.all(promises);\n }\n /**\n * Persist user language preference to API.\n * PATCH {userLanguageEndpoint} { language: locale }\n */\n private async persistUserLanguage(locale: string): Promise<void> {\n if (this.userLanguageEndpoint === false) return;\n\n try {\n if (this.client) {\n await this.client.patch(this.userLanguageEndpoint, { language: locale });\n } else if (this.getAccessToken && this.apiUrl) {\n const token = await this.getAccessToken();\n\n if (!token) return;\n\n const url = `${this.apiUrl.replace(/\\/$/, '')}/${this.userLanguageEndpoint}`;\n\n await fetch(url, {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ language: locale })\n });\n }\n } catch {\n // Language persist is best-effort — don't block locale switch on failure\n }\n }\n /**\n * Update config at runtime.\n * Useful when client transitions from null → RestApiClient (auth loading complete).\n */\n updateConfig(updates: Partial<Pick<DocyrusI18nConfig, 'client' | 'staticTranslations'>>): void {\n let shouldFetch = false;\n\n if (updates.client !== undefined && updates.client !== this.client) {\n const hadNoClient = !this.client;\n\n this.client = updates.client;\n\n // Client became available — fetch translations\n if (hadNoClient && this.client && !this.disableApiFetch) {\n shouldFetch = true;\n }\n }\n\n if (updates.staticTranslations !== undefined) {\n this.staticTranslations = updates.staticTranslations;\n this.mergeTranslations();\n this.notify();\n }\n\n if (shouldFetch) {\n this.fetchTranslations();\n }\n }\n /** Cleanup: abort in-flight requests + clear listeners. */\n destroy(): void {\n this.abortController?.abort();\n this.listeners.clear();\n }\n private mergeTranslations(): void {\n if (this.mergeStrategy === 'api-first') {\n this.translations = { ...this.staticTranslations, ...this.apiTranslations };\n } else {\n this.translations = { ...this.apiTranslations, ...this.staticTranslations };\n }\n }\n}\n","\nimport { type RestApiClient } from '@docyrus/api-client';\n\nimport {\n ref,\n shallowRef,\n computed,\n defineComponent,\n inject,\n provide,\n watch,\n onMounted,\n onUnmounted,\n type InjectionKey,\n type PropType\n} from 'vue';\n\nimport {\n type DocyrusI18nConfig,\n type FallbackStrategy,\n type I18nStatus,\n type TranslationDictionary\n} from '../types';\n\nimport { I18nManager } from '../core/i18n-manager';\n\n// ─── Internal reactive i18n state ────────────────────────────────\n\ninterface DocyrusI18nState {\n status: I18nStatus;\n locale: string | null;\n translations: TranslationDictionary;\n error: Error | null;\n isLoading: boolean;\n t: (key: string, params?: Record<string, string | number>) => string;\n setLocale: (locale: string) => void;\n refetch: () => Promise<void>;\n}\n\nconst DOCYRUS_I18N_KEY: InjectionKey<DocyrusI18nState> = Symbol('docyrus-i18n');\n\n// ─── Composables ─────────────────────────────────────────────────\n\n/**\n * Vue composable to access full Docyrus i18n state.\n * Must be used within a `<DocyrusI18nProvider>`.\n *\n * Returns: { t, locale, setLocale, status, isLoading, translations, error, refetch }\n */\nexport function useDocyrusI18n(): DocyrusI18nState {\n const context = inject(DOCYRUS_I18N_KEY);\n\n if (!context) {\n throw new Error(\n 'useDocyrusI18n() must be used within a <DocyrusI18nProvider>. '\n + 'Wrap your app with <DocyrusI18nProvider> to provide i18n context.'\n );\n }\n\n return context;\n}\n\n/**\n * Lightweight composable for translation only.\n * Returns just `t` and `isLoading`.\n */\nexport function useTranslation() {\n const context = useDocyrusI18n();\n\n return {\n t: computed(() => context.t),\n isLoading: computed(() => context.isLoading)\n };\n}\n\n// ─── Components ──────────────────────────────────────────────────\n\n/**\n * Vue provider component that wraps your app with Docyrus i18n context.\n *\n * Usage:\n * ```vue\n * <template>\n * <DocyrusI18nProvider :client=\"client\">\n * <App />\n * </DocyrusI18nProvider>\n * </template>\n *\n * <script setup>\n * import { DocyrusI18nProvider } from '@docyrus/i18n/vue';\n * </script>\n * ```\n */\nexport const DocyrusI18nProvider = defineComponent({\n name: 'DocyrusI18nProvider',\n props: {\n client: { type: Object as PropType<RestApiClient | null>, default: null },\n getAccessToken: { type: Function as PropType<() => Promise<string | null> | string | null>, default: undefined },\n apiUrl: { type: String, default: undefined },\n translationsEndpoint: { type: String, default: undefined },\n staticTranslations: { type: Object as PropType<TranslationDictionary>, default: undefined },\n cookieKey: { type: String, default: undefined },\n mergeStrategy: { type: String as PropType<'api-first' | 'static-first'>, default: undefined },\n fallback: { type: [String, Function] as PropType<FallbackStrategy>, default: undefined },\n disableApiFetch: { type: Boolean, default: undefined }\n },\n setup(props, { slots }) {\n const status = ref<I18nStatus>('loading');\n const locale = ref<string | null>(null);\n const translations = shallowRef<TranslationDictionary>({});\n const error = shallowRef<Error | null>(null);\n\n let manager: I18nManager | null = null;\n\n const setLocale = (newLocale: string) => {\n manager?.setLocale(newLocale);\n };\n\n const refetch = async () => {\n await manager?.fetchTranslations();\n };\n\n const t = (key: string, params?: Record<string, string | number>) => {\n return manager?.t(key, params) ?? key;\n };\n\n const i18nState: DocyrusI18nState = {\n get status() { return status.value; },\n get locale() { return locale.value; },\n get translations() { return translations.value; },\n get error() { return error.value; },\n get isLoading() { return status.value === 'loading'; },\n t,\n setLocale,\n refetch\n };\n\n provide(DOCYRUS_I18N_KEY, i18nState);\n\n onMounted(() => {\n const config: DocyrusI18nConfig = {\n client: props.client,\n getAccessToken: props.getAccessToken,\n apiUrl: props.apiUrl,\n translationsEndpoint: props.translationsEndpoint,\n staticTranslations: props.staticTranslations,\n cookieKey: props.cookieKey,\n mergeStrategy: props.mergeStrategy,\n fallback: props.fallback,\n disableApiFetch: props.disableApiFetch\n };\n\n manager = new I18nManager(config);\n locale.value = manager.getLocale();\n\n manager.subscribe((state) => {\n status.value = state.status;\n locale.value = state.locale;\n translations.value = state.translations;\n error.value = state.error;\n });\n\n manager.initialize();\n });\n\n // Watch client prop for null → value transition\n watch(() => props.client, (newClient) => {\n manager?.updateConfig({ client: newClient });\n });\n\n onUnmounted(() => {\n manager?.destroy();\n manager = null;\n });\n\n return () => slots.default?.();\n }\n});\n\n// ─── Type re-exports ─────────────────────────────────────────────\n\nexport type {\n DocyrusI18nState,\n DocyrusI18nConfig,\n I18nStatus,\n TranslationDictionary,\n FallbackStrategy\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docyrus/i18n",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"tsup": "8.5.1",
|
|
37
37
|
"typescript": "5.9.3",
|
|
38
38
|
"vue": "3.5.28",
|
|
39
|
-
"@docyrus/api-client": "0.0
|
|
39
|
+
"@docyrus/api-client": "0.1.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@docyrus/api-client": ">=0.0.9",
|