@fayz-ai/core 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/dist/app/render.d.ts +16 -0
  2. package/dist/app/render.d.ts.map +1 -0
  3. package/dist/blocks/index.d.ts +25 -0
  4. package/dist/blocks/index.d.ts.map +1 -0
  5. package/dist/chunk-2NTHNWEH.cjs +32 -0
  6. package/dist/chunk-2NTHNWEH.cjs.map +1 -0
  7. package/dist/chunk-5EAZOMVP.js +1609 -0
  8. package/dist/chunk-5EAZOMVP.js.map +1 -0
  9. package/dist/chunk-5TKIZVVJ.cjs +16 -0
  10. package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
  11. package/dist/chunk-7RTUHSKY.js +3 -0
  12. package/dist/chunk-7RTUHSKY.js.map +1 -0
  13. package/dist/chunk-AXTWKF5X.cjs +38 -0
  14. package/dist/chunk-AXTWKF5X.cjs.map +1 -0
  15. package/dist/chunk-EGWWQ7AT.cjs +355 -0
  16. package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
  17. package/dist/chunk-KAYQFSR6.js +298 -0
  18. package/dist/chunk-KAYQFSR6.js.map +1 -0
  19. package/dist/chunk-LSVBDYFQ.cjs +1639 -0
  20. package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
  21. package/dist/chunk-SNQW6TRF.cjs +658 -0
  22. package/dist/chunk-SNQW6TRF.cjs.map +1 -0
  23. package/dist/chunk-WKZVSKEU.js +26 -0
  24. package/dist/chunk-WKZVSKEU.js.map +1 -0
  25. package/dist/chunk-Y2C2FBQP.js +35 -0
  26. package/dist/chunk-Y2C2FBQP.js.map +1 -0
  27. package/dist/chunk-YSBXGHAM.js +643 -0
  28. package/dist/chunk-YSBXGHAM.js.map +1 -0
  29. package/dist/data/archetype.d.ts +13 -0
  30. package/dist/data/archetype.d.ts.map +1 -0
  31. package/dist/data/cached.d.ts +19 -0
  32. package/dist/data/cached.d.ts.map +1 -0
  33. package/dist/data/fayz-api.d.ts +19 -0
  34. package/dist/data/fayz-api.d.ts.map +1 -0
  35. package/dist/data/index.cjs +32 -0
  36. package/dist/data/index.cjs.map +1 -0
  37. package/dist/data/index.d.ts +9 -0
  38. package/dist/data/index.d.ts.map +1 -0
  39. package/dist/data/index.js +3 -0
  40. package/dist/data/index.js.map +1 -0
  41. package/dist/data/mock.d.ts +7 -0
  42. package/dist/data/mock.d.ts.map +1 -0
  43. package/dist/data/resolve.d.ts +22 -0
  44. package/dist/data/resolve.d.ts.map +1 -0
  45. package/dist/data/supabase.d.ts +18 -0
  46. package/dist/data/supabase.d.ts.map +1 -0
  47. package/dist/data/types.d.ts +23 -0
  48. package/dist/data/types.d.ts.map +1 -0
  49. package/dist/entity/index.cjs +28 -0
  50. package/dist/entity/index.cjs.map +1 -0
  51. package/dist/entity/index.d.ts +3 -0
  52. package/dist/entity/index.d.ts.map +1 -0
  53. package/dist/entity/index.js +3 -0
  54. package/dist/entity/index.js.map +1 -0
  55. package/dist/entity/registry.d.ts +20 -0
  56. package/dist/entity/registry.d.ts.map +1 -0
  57. package/dist/events/index.d.ts +16 -0
  58. package/dist/events/index.d.ts.map +1 -0
  59. package/dist/i18n/index.cjs +44 -0
  60. package/dist/i18n/index.cjs.map +1 -0
  61. package/dist/i18n/index.d.ts +21 -0
  62. package/dist/i18n/index.d.ts.map +1 -0
  63. package/dist/i18n/index.js +3 -0
  64. package/dist/i18n/index.js.map +1 -0
  65. package/dist/i18n/shell-translations.d.ts +2 -0
  66. package/dist/i18n/shell-translations.d.ts.map +1 -0
  67. package/dist/index.cjs +911 -0
  68. package/dist/index.cjs.map +1 -0
  69. package/dist/index.d.ts +46 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +596 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/lib/cache.d.ts +21 -0
  74. package/dist/lib/cache.d.ts.map +1 -0
  75. package/dist/lib/csv.d.ts +14 -0
  76. package/dist/lib/csv.d.ts.map +1 -0
  77. package/dist/lib/format.d.ts +7 -0
  78. package/dist/lib/format.d.ts.map +1 -0
  79. package/dist/manifest/index.d.ts +80 -0
  80. package/dist/manifest/index.d.ts.map +1 -0
  81. package/dist/plugin/createSafeDataProvider.d.ts +14 -0
  82. package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
  83. package/dist/plugin/index.cjs +32 -0
  84. package/dist/plugin/index.cjs.map +1 -0
  85. package/dist/plugin/index.d.ts +2 -0
  86. package/dist/plugin/index.d.ts.map +1 -0
  87. package/dist/plugin/index.js +3 -0
  88. package/dist/plugin/index.js.map +1 -0
  89. package/dist/plugin/runtime.d.ts +31 -0
  90. package/dist/plugin/runtime.d.ts.map +1 -0
  91. package/dist/registry/index.d.ts +88 -0
  92. package/dist/registry/index.d.ts.map +1 -0
  93. package/dist/router/index.cjs +16 -0
  94. package/dist/router/index.cjs.map +1 -0
  95. package/dist/router/index.d.ts +8 -0
  96. package/dist/router/index.d.ts.map +1 -0
  97. package/dist/router/index.js +3 -0
  98. package/dist/router/index.js.map +1 -0
  99. package/dist/runtime/index.cjs +16 -0
  100. package/dist/runtime/index.cjs.map +1 -0
  101. package/dist/runtime/index.d.ts +3 -0
  102. package/dist/runtime/index.d.ts.map +1 -0
  103. package/dist/runtime/index.js +3 -0
  104. package/dist/runtime/index.js.map +1 -0
  105. package/dist/runtime/oauth.d.ts +96 -0
  106. package/dist/runtime/oauth.d.ts.map +1 -0
  107. package/dist/tenant/index.d.ts +3 -0
  108. package/dist/tenant/index.d.ts.map +1 -0
  109. package/dist/types/auth.d.ts +27 -0
  110. package/dist/types/auth.d.ts.map +1 -0
  111. package/dist/types/billing.d.ts +17 -0
  112. package/dist/types/billing.d.ts.map +1 -0
  113. package/dist/types/crud.d.ts +107 -0
  114. package/dist/types/crud.d.ts.map +1 -0
  115. package/dist/types/entities.d.ts +134 -0
  116. package/dist/types/entities.d.ts.map +1 -0
  117. package/dist/types/i18n.d.ts +8 -0
  118. package/dist/types/i18n.d.ts.map +1 -0
  119. package/dist/types/index.cjs +4 -0
  120. package/dist/types/index.cjs.map +1 -0
  121. package/dist/types/index.d.ts +10 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +3 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/types/org.d.ts +96 -0
  126. package/dist/types/org.d.ts.map +1 -0
  127. package/dist/types/permissions.d.ts +25 -0
  128. package/dist/types/permissions.d.ts.map +1 -0
  129. package/dist/types/plugins.d.ts +314 -0
  130. package/dist/types/plugins.d.ts.map +1 -0
  131. package/dist/types/theme.d.ts +16 -0
  132. package/dist/types/theme.d.ts.map +1 -0
  133. package/package.json +98 -0
  134. package/src/app/render.tsx +69 -0
  135. package/src/blocks/index.tsx +68 -0
  136. package/src/data/archetype.ts +210 -0
  137. package/src/data/cached.ts +55 -0
  138. package/src/data/fayz-api.ts +200 -0
  139. package/src/data/index.ts +8 -0
  140. package/src/data/mock.ts +84 -0
  141. package/src/data/resolve.ts +108 -0
  142. package/src/data/supabase.ts +169 -0
  143. package/src/data/types.ts +20 -0
  144. package/src/entity/index.ts +2 -0
  145. package/src/entity/registry.ts +42 -0
  146. package/src/events/index.tsx +82 -0
  147. package/src/i18n/index.ts +250 -0
  148. package/src/i18n/shell-translations.ts +1391 -0
  149. package/src/index.ts +132 -0
  150. package/src/lib/cache.ts +118 -0
  151. package/src/lib/csv.ts +54 -0
  152. package/src/lib/format.ts +58 -0
  153. package/src/manifest/app-manifest.schema.json +103 -0
  154. package/src/manifest/index.ts +394 -0
  155. package/src/plugin/createSafeDataProvider.ts +30 -0
  156. package/src/plugin/index.ts +1 -0
  157. package/src/plugin/runtime.ts +313 -0
  158. package/src/registry/index.ts +163 -0
  159. package/src/router/index.ts +29 -0
  160. package/src/runtime/index.ts +15 -0
  161. package/src/runtime/oauth.ts +219 -0
  162. package/src/tenant/index.ts +18 -0
  163. package/src/types/auth.ts +26 -0
  164. package/src/types/billing.ts +17 -0
  165. package/src/types/crud.ts +108 -0
  166. package/src/types/entities.ts +142 -0
  167. package/src/types/i18n.ts +7 -0
  168. package/src/types/index.ts +10 -0
  169. package/src/types/org.ts +94 -0
  170. package/src/types/permissions.ts +32 -0
  171. package/src/types/plugins.ts +363 -0
  172. package/src/types/theme.ts +17 -0
@@ -0,0 +1,169 @@
1
+ import type { DataProvider, CrudQuery, CrudResult } from './types'
2
+
3
+ export interface SupabaseProviderConfig {
4
+ schema?: string
5
+ tenantId?: string | (() => string | undefined)
6
+ tenantIdColumn?: string
7
+ searchColumns?: string[]
8
+ selectColumns?: string
9
+ columnMap?: Record<string, string>
10
+ filters?: Record<string, string>
11
+ defaults?: Record<string, unknown>
12
+ supabaseClient?: unknown
13
+ }
14
+
15
+ function camelToSnake(str: string): string {
16
+ return str.replace(/[A-Z]/g, (c) => '_' + c.toLowerCase())
17
+ }
18
+
19
+ function snakeToCamel(str: string): string {
20
+ return str.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase())
21
+ }
22
+
23
+ function mapRow<T>(row: Record<string, unknown>, columnMap?: Record<string, string>): T {
24
+ const result: Record<string, unknown> = {}
25
+ for (const [key, value] of Object.entries(row)) {
26
+ const camelKey = columnMap?.[key] ?? snakeToCamel(key)
27
+ result[camelKey] = value
28
+ }
29
+ return result as T
30
+ }
31
+
32
+ function mapEntity(entity: Record<string, unknown>, columnMap?: Record<string, string>): Record<string, unknown> {
33
+ const reverseMap: Record<string, string> = {}
34
+ if (columnMap) {
35
+ for (const [snake, camel] of Object.entries(columnMap)) {
36
+ reverseMap[camel] = snake
37
+ }
38
+ }
39
+ const result: Record<string, unknown> = {}
40
+ for (const [key, value] of Object.entries(entity)) {
41
+ if (key === 'id' || key === 'createdAt' || key === 'updatedAt' || key === 'tenantId') continue
42
+ const snakeKey = reverseMap[key] ?? camelToSnake(key)
43
+ result[snakeKey] = value
44
+ }
45
+ return result
46
+ }
47
+
48
+ // Global Supabase client registry — set once via createFayzApp
49
+ let _globalSupabaseClient: unknown | null = null
50
+
51
+ export function setGlobalSupabaseClient(client: unknown): void {
52
+ _globalSupabaseClient = client
53
+ }
54
+
55
+ export function getSupabaseClientOptional(): unknown | null {
56
+ return _globalSupabaseClient
57
+ }
58
+
59
+ export function createSupabaseProvider<T extends { id: string }>(
60
+ table: string,
61
+ config?: SupabaseProviderConfig,
62
+ ): DataProvider<T> {
63
+ const schema = config?.schema ?? 'public'
64
+ const tenantIdCol = config?.tenantIdColumn ?? 'tenant_id'
65
+ const searchCols = config?.searchColumns ?? []
66
+ const selectCols = config?.selectColumns ?? '*'
67
+ const columnMap = config?.columnMap
68
+
69
+ function resolveTenantId(): string | undefined {
70
+ if (!config?.tenantId) return undefined
71
+ return typeof config.tenantId === 'function' ? config.tenantId() : config.tenantId
72
+ }
73
+
74
+ function getClient() {
75
+ const supabase = (config?.supabaseClient ?? _globalSupabaseClient) as {
76
+ schema: (s: string) => unknown
77
+ from: (t: string) => unknown
78
+ } | null
79
+ if (!supabase) throw new Error(`[@fayz-ai/core] Supabase client not available. Call createFayzApp with a provider first.`)
80
+ return schema === 'public' ? (supabase as { from: (t: string) => unknown }) : (supabase.schema(schema) as { from: (t: string) => unknown })
81
+ }
82
+
83
+ return {
84
+ async list(query: CrudQuery): Promise<CrudResult<T>> {
85
+ let q = (getClient().from(table) as { select: (...args: unknown[]) => unknown })
86
+ .select(selectCols, { count: 'exact' }) as Record<string, unknown>
87
+
88
+ const tenantId = resolveTenantId()
89
+ if (tenantId) q = (q as { eq: (col: string, val: string) => unknown }).eq(tenantIdCol, tenantId) as Record<string, unknown>
90
+
91
+ if (config?.filters) {
92
+ for (const [col, val] of Object.entries(config.filters)) {
93
+ q = (q as { eq: (c: string, v: string) => unknown }).eq(col, val) as Record<string, unknown>
94
+ }
95
+ }
96
+
97
+ // Faceted / dynamic filters from the query (equality, mapped to columns).
98
+ if (query.filters) {
99
+ for (const [key, val] of Object.entries(query.filters)) {
100
+ if (val == null || val === '') continue
101
+ const col = columnMap?.[key] ?? camelToSnake(key)
102
+ q = (q as { eq: (c: string, v: unknown) => unknown }).eq(col, val) as Record<string, unknown>
103
+ }
104
+ }
105
+
106
+ if (query.search && searchCols.length > 0) {
107
+ const term = `%${query.search}%`
108
+ const orClause = searchCols.map((col) => `${camelToSnake(col)}.ilike.${term}`).join(',')
109
+ q = (q as { or: (c: string) => unknown }).or(orClause) as Record<string, unknown>
110
+ }
111
+
112
+ if (query.sortBy) {
113
+ q = (q as { order: (col: string, opts: unknown) => unknown })
114
+ .order(camelToSnake(query.sortBy), { ascending: query.sortDir !== 'desc' }) as Record<string, unknown>
115
+ } else {
116
+ q = (q as { order: (col: string, opts: unknown) => unknown })
117
+ .order('created_at', { ascending: false }) as Record<string, unknown>
118
+ }
119
+
120
+ const page = query.page ?? 1
121
+ const pageSize = query.pageSize ?? 50
122
+ const from = (page - 1) * pageSize
123
+ q = (q as { range: (f: number, t: number) => unknown }).range(from, from + pageSize - 1) as Record<string, unknown>
124
+
125
+ const { data, error, count } = await q as { data: Record<string, unknown>[] | null; error: unknown; count: number | null }
126
+ if (error) throw error
127
+
128
+ return {
129
+ data: (data ?? []).map((row) => mapRow<T>(row, columnMap)),
130
+ total: count ?? 0,
131
+ }
132
+ },
133
+
134
+ async create(data) {
135
+ const row = mapEntity(data as Record<string, unknown>, columnMap)
136
+ const tenantId = resolveTenantId()
137
+ if (tenantId) row[tenantIdCol] = tenantId
138
+ if (config?.defaults) {
139
+ for (const [col, val] of Object.entries(config.defaults)) {
140
+ if (row[col] === undefined) row[col] = val
141
+ }
142
+ }
143
+
144
+ const { data: created, error } = (await (getClient().from(table) as {
145
+ insert: (r: unknown) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } }
146
+ }).insert(row).select().single()) as { data: Record<string, unknown> | null; error: unknown }
147
+
148
+ if (error) throw error
149
+ return mapRow<T>(created!, columnMap)
150
+ },
151
+
152
+ async update(id, data) {
153
+ const row = mapEntity(data as Record<string, unknown>, columnMap)
154
+ const { data: updated, error } = (await (getClient().from(table) as {
155
+ update: (r: unknown) => { eq: (c: string, v: string) => { select: () => { single: () => Promise<{ data: unknown; error: unknown }> } } }
156
+ }).update(row).eq('id', id).select().single()) as { data: Record<string, unknown> | null; error: unknown }
157
+
158
+ if (error) throw error
159
+ return mapRow<T>(updated!, columnMap)
160
+ },
161
+
162
+ async remove(id) {
163
+ const { error } = (await (getClient().from(table) as {
164
+ delete: () => { eq: (c: string, v: string) => Promise<{ error: unknown }> }
165
+ }).delete().eq('id', id)) as { error: unknown }
166
+ if (error) throw error
167
+ },
168
+ }
169
+ }
@@ -0,0 +1,20 @@
1
+ export interface CrudQuery {
2
+ search?: string
3
+ sortBy?: string
4
+ sortDir?: 'asc' | 'desc'
5
+ page?: number
6
+ pageSize?: number
7
+ filters?: Record<string, unknown>
8
+ }
9
+
10
+ export interface CrudResult<T> {
11
+ data: T[]
12
+ total: number
13
+ }
14
+
15
+ export interface DataProvider<T extends { id: string } = { id: string }> {
16
+ list(query: CrudQuery): Promise<CrudResult<T>>
17
+ create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt' | 'tenantId'>): Promise<T>
18
+ update(id: string, data: Partial<T>): Promise<T>
19
+ remove(id: string): Promise<void>
20
+ }
@@ -0,0 +1,2 @@
1
+ export { registerEntity, getEntityByKey, getAllEntities, clearEntityRegistry, deriveEntityKey } from './registry'
2
+ export type { RegisteredEntity } from './registry'
@@ -0,0 +1,42 @@
1
+ import type { EntityDef } from '../types/crud'
2
+ import type { EntityArchetype } from '../types/entities'
3
+
4
+ export interface RegisteredEntity {
5
+ entityKey: string
6
+ label: string
7
+ labelPlural: string
8
+ icon?: string
9
+ fields: EntityDef['fields']
10
+ source: 'app' | 'plugin'
11
+ archetype?: EntityArchetype
12
+ pluginId?: string
13
+ pluginName?: string
14
+ pluginIcon?: string
15
+ }
16
+
17
+ const entityRegistry = new Map<string, RegisteredEntity>()
18
+
19
+ export function registerEntity(entry: RegisteredEntity): void {
20
+ entityRegistry.set(entry.entityKey, entry)
21
+ }
22
+
23
+ export function getEntityByKey(key: string): RegisteredEntity | undefined {
24
+ return entityRegistry.get(key)
25
+ }
26
+
27
+ export function getAllEntities(): RegisteredEntity[] {
28
+ return Array.from(entityRegistry.values())
29
+ }
30
+
31
+ export function clearEntityRegistry(): void {
32
+ entityRegistry.clear()
33
+ }
34
+
35
+ export function deriveEntityKey(entityDef: EntityDef): string {
36
+ const name = entityDef.name.toLowerCase().replace(/\s+/g, '-')
37
+ if (entityDef.data?.archetype && entityDef.data?.archetypeKind) {
38
+ return `${entityDef.data.archetype}:${entityDef.data.archetypeKind}`
39
+ }
40
+ if (entityDef.data?.archetype) return entityDef.data.archetype
41
+ return name
42
+ }
@@ -0,0 +1,82 @@
1
+ import * as React from 'react'
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Event bus — the substrate for cross-plugin behavior. Replaces the bespoke
5
+ // `window.dispatchEvent('agenda:open-booking')` hack and the imperative
6
+ // financial bridge: plugins emit named events, others subscribe, and the
7
+ // AppManifest can declare event→action bindings as data. Plugins describe the
8
+ // events they emit via PluginManifest.events (see types/plugins) so the
9
+ // platform can introspect and wire automations.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export type EventHandler<T = unknown> = (payload: T) => void
13
+
14
+ export interface EventBus {
15
+ emit<T = unknown>(event: string, payload?: T): void
16
+ on<T = unknown>(event: string, handler: EventHandler<T>): () => void
17
+ once<T = unknown>(event: string, handler: EventHandler<T>): () => void
18
+ off<T = unknown>(event: string, handler: EventHandler<T>): void
19
+ clear(event?: string): void
20
+ }
21
+
22
+ export function createEventBus(): EventBus {
23
+ const handlers = new Map<string, Set<EventHandler>>()
24
+ const bus: EventBus = {
25
+ emit(event, payload) {
26
+ const set = handlers.get(event)
27
+ if (!set) return
28
+ // Snapshot so handlers can subscribe/unsubscribe during dispatch.
29
+ for (const handler of Array.from(set)) {
30
+ try {
31
+ handler(payload)
32
+ } catch (err) {
33
+ // A misbehaving subscriber must not break the emitter or its peers.
34
+ console.error(`[fayz] event handler for "${event}" threw:`, err)
35
+ }
36
+ }
37
+ },
38
+ on(event, handler) {
39
+ let set = handlers.get(event)
40
+ if (!set) {
41
+ set = new Set()
42
+ handlers.set(event, set)
43
+ }
44
+ set.add(handler as EventHandler)
45
+ return () => bus.off(event, handler)
46
+ },
47
+ once(event, handler) {
48
+ const wrapped: EventHandler = (payload) => {
49
+ bus.off(event, wrapped)
50
+ ;(handler as EventHandler)(payload)
51
+ }
52
+ return bus.on(event, wrapped)
53
+ },
54
+ off(event, handler) {
55
+ handlers.get(event)?.delete(handler as EventHandler)
56
+ },
57
+ clear(event) {
58
+ if (event) handlers.delete(event)
59
+ else handlers.clear()
60
+ },
61
+ }
62
+ return bus
63
+ }
64
+
65
+ /** The app-wide default bus. One per app runtime (repo-per-app). */
66
+ export const eventBus: EventBus = createEventBus()
67
+
68
+ /** Subscribe to an event for the lifetime of the component. The latest handler
69
+ * is always invoked, so closures over fresh props/state work without resubscribing. */
70
+ export function useOnEvent<T = unknown>(
71
+ event: string,
72
+ handler: EventHandler<T>,
73
+ deps: React.DependencyList = [],
74
+ ): void {
75
+ const ref = React.useRef(handler)
76
+ ref.current = handler
77
+ React.useEffect(
78
+ () => eventBus.on<T>(event, (payload) => ref.current(payload)),
79
+ // eslint-disable-next-line react-hooks/exhaustive-deps
80
+ [event, ...deps],
81
+ )
82
+ }
@@ -0,0 +1,250 @@
1
+ import * as React from 'react'
2
+ import { shellTranslations } from './shell-translations'
3
+
4
+ export interface I18nConfig {
5
+ defaultLocale: string
6
+ supported: string[]
7
+ translations: Record<string, Record<string, string>>
8
+ }
9
+
10
+ const I18nContext = React.createContext<I18nConfig>({
11
+ defaultLocale: 'en',
12
+ supported: ['en'],
13
+ translations: {},
14
+ })
15
+
16
+ export const I18nProvider = I18nContext.Provider
17
+
18
+ export function useI18nConfig(): I18nConfig {
19
+ return React.useContext(I18nContext)
20
+ }
21
+
22
+ export function useTranslation(): (key: string, params?: Record<string, string | number>) => string {
23
+ const config = React.useContext(I18nContext)
24
+ const localeStore = useLocaleStore()
25
+ const locale = localeStore.locale
26
+
27
+ return React.useCallback(
28
+ (key: string, params?: Record<string, string | number>): string => {
29
+ const localizedTranslations = config.translations[locale] ?? {}
30
+ const fallback = config.translations[config.defaultLocale] ?? {}
31
+ // Global fallback lets plugins register their locales independently of any
32
+ // I18nProvider — needed when a de-bridged plugin runs under a host shell
33
+ // that doesn't mount @fayz-ai/core's I18nProvider (incremental de-bridge), and
34
+ // for manifest apps where plugins self-register translations.
35
+ const gLocale = _globalTranslations[locale] ?? {}
36
+ const gFallback = _globalTranslations[config.defaultLocale] ?? {}
37
+ let raw = localizedTranslations[key] ?? gLocale[key] ?? fallback[key] ?? gFallback[key] ?? key
38
+
39
+ if (params) {
40
+ for (const [k, v] of Object.entries(params)) {
41
+ raw = raw.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v))
42
+ }
43
+ }
44
+ return raw
45
+ },
46
+ [config, locale],
47
+ )
48
+ }
49
+
50
+ // Minimal locale store (avoids circular dep with stores package)
51
+ interface LocaleStore {
52
+ locale: string
53
+ setLocale: (locale: string) => void
54
+ }
55
+
56
+ let _localeStore: LocaleStore = {
57
+ locale: 'en',
58
+ setLocale: (l) => { _localeStore.locale = l },
59
+ }
60
+
61
+ export function setLocaleStore(store: LocaleStore): void {
62
+ _localeStore = store
63
+ }
64
+
65
+ function useLocaleStore(): LocaleStore {
66
+ const [, rerender] = React.useReducer((n: number) => n + 1, 0)
67
+ React.useEffect(() => {
68
+ const prev = _localeStore.setLocale
69
+ _localeStore.setLocale = (l) => {
70
+ prev(l)
71
+ rerender()
72
+ }
73
+ return () => { _localeStore.setLocale = prev }
74
+ }, [])
75
+ return _localeStore
76
+ }
77
+
78
+ export function getCurrentLocale(): string {
79
+ return _localeStore.locale
80
+ }
81
+
82
+ export function setCurrentLocale(locale: string): void {
83
+ _localeStore.setLocale(locale)
84
+ }
85
+
86
+ // Core built-in translations
87
+ export const coreTranslations: Record<string, Record<string, string>> = {
88
+ en: {
89
+ 'common.save': 'Save',
90
+ 'common.saveChanges': 'Save Changes',
91
+ 'common.saving': 'Saving...',
92
+ 'common.cancel': 'Cancel',
93
+ 'common.delete': 'Delete',
94
+ 'common.edit': 'Edit',
95
+ 'common.close': 'Close',
96
+ 'common.confirm': 'Confirm',
97
+ 'common.discard': 'Discard',
98
+ 'common.back': 'Back',
99
+ 'common.formIncomplete': 'Fill in the required fields before saving.',
100
+ 'saveBar.unsaved': 'Unsaved changes',
101
+ 'common.add': 'Add',
102
+ 'common.loading': 'Loading...',
103
+ 'common.error': 'Something went wrong',
104
+ 'common.noResults': 'No results found.',
105
+ 'common.search': 'Search',
106
+ 'common.actions': 'Actions',
107
+ 'common.continue': 'Continue',
108
+ 'common.done': 'Done',
109
+ 'common.yes': 'Yes',
110
+ 'common.no': 'No',
111
+ 'common.active': 'Active',
112
+ 'common.inactive': 'Inactive',
113
+ 'common.overview': 'Overview',
114
+ 'common.settings': 'Settings',
115
+ 'common.plugins': 'Plugins',
116
+ 'common.new': 'New',
117
+ 'common.filter': 'Filter',
118
+ 'common.export': 'Export',
119
+ 'common.import': 'Import',
120
+ 'common.preview': 'Preview',
121
+ 'common.total': 'Total',
122
+ 'crud.newItem': 'New {{name}}',
123
+ 'crud.editItem': 'Edit {{name}}',
124
+ 'crud.deleteConfirm': 'Delete {{name}}?',
125
+ 'crud.deleteMessage': 'This action cannot be undone.',
126
+ 'crud.created': '{{name}} created',
127
+ 'crud.updated': '{{name}} updated',
128
+ 'crud.deleted': '{{name}} deleted',
129
+ 'crud.noItems': 'No {{namePlural}} yet.',
130
+ 'auth.login.title': 'Sign in',
131
+ 'auth.login.subtitle': 'Enter your credentials to access your account',
132
+ 'auth.login.emailLabel': 'Email',
133
+ 'auth.login.passwordLabel': 'Password',
134
+ 'auth.login.submit': 'Sign in',
135
+ 'auth.login.forgotPassword': 'Forgot password?',
136
+ 'auth.login.noAccount': "Don't have an account?",
137
+ 'auth.login.signUpLink': 'Sign up',
138
+ 'auth.signup.title': 'Create account',
139
+ 'auth.signup.submit': 'Create account',
140
+ 'auth.signup.hasAccount': 'Already have an account?',
141
+ 'auth.signup.signInLink': 'Sign in',
142
+ 'settings.title': 'Settings',
143
+ 'settings.company': 'Company',
144
+ 'settings.branding': 'Branding',
145
+ 'settings.security': 'Security',
146
+ 'settings.team': 'Team',
147
+ 'settings.billing': 'Billing',
148
+ 'settings.locations': 'Locations',
149
+ 'billing.title': 'Plans & Billing',
150
+ 'billing.currentPlan': 'Current Plan',
151
+ 'billing.upgrade': 'Upgrade',
152
+ 'billing.manage': 'Manage Subscription',
153
+ },
154
+ 'pt-BR': {
155
+ 'common.save': 'Salvar',
156
+ 'common.saveChanges': 'Salvar Alterações',
157
+ 'common.saving': 'Salvando...',
158
+ 'common.cancel': 'Cancelar',
159
+ 'common.delete': 'Excluir',
160
+ 'common.edit': 'Editar',
161
+ 'common.close': 'Fechar',
162
+ 'common.confirm': 'Confirmar',
163
+ 'common.discard': 'Descartar',
164
+ 'common.back': 'Voltar',
165
+ 'common.formIncomplete': 'Preencha os campos obrigatórios antes de salvar.',
166
+ 'saveBar.unsaved': 'Alterações não salvas',
167
+ 'common.add': 'Adicionar',
168
+ 'common.loading': 'Carregando...',
169
+ 'common.error': 'Algo deu errado',
170
+ 'common.noResults': 'Nenhum resultado encontrado.',
171
+ 'common.search': 'Buscar',
172
+ 'common.actions': 'Ações',
173
+ 'common.continue': 'Continuar',
174
+ 'common.done': 'Concluído',
175
+ 'common.yes': 'Sim',
176
+ 'common.no': 'Não',
177
+ 'common.active': 'Ativo',
178
+ 'common.inactive': 'Inativo',
179
+ 'common.overview': 'Visão Geral',
180
+ 'common.settings': 'Configurações',
181
+ 'common.plugins': 'Plugins',
182
+ 'common.new': 'Novo',
183
+ 'common.filter': 'Filtrar',
184
+ 'common.export': 'Exportar',
185
+ 'common.import': 'Importar',
186
+ 'common.preview': 'Pré-visualizar',
187
+ 'common.total': 'Total',
188
+ 'crud.newItem': 'Novo {{name}}',
189
+ 'crud.editItem': 'Editar {{name}}',
190
+ 'crud.deleteConfirm': 'Excluir {{name}}?',
191
+ 'crud.deleteMessage': 'Esta ação não pode ser desfeita.',
192
+ 'crud.created': '{{name}} criado',
193
+ 'crud.updated': '{{name}} atualizado',
194
+ 'crud.deleted': '{{name}} excluído',
195
+ 'crud.noItems': 'Nenhum {{namePlural}} ainda.',
196
+ 'auth.login.title': 'Entrar',
197
+ 'auth.login.subtitle': 'Digite suas credenciais para acessar sua conta',
198
+ 'auth.login.emailLabel': 'E-mail',
199
+ 'auth.login.passwordLabel': 'Senha',
200
+ 'auth.login.submit': 'Entrar',
201
+ 'auth.login.forgotPassword': 'Esqueceu a senha?',
202
+ 'auth.login.noAccount': 'Não tem uma conta?',
203
+ 'auth.login.signUpLink': 'Cadastrar',
204
+ 'auth.signup.title': 'Criar conta',
205
+ 'auth.signup.submit': 'Criar conta',
206
+ 'auth.signup.hasAccount': 'Já tem uma conta?',
207
+ 'auth.signup.signInLink': 'Entrar',
208
+ 'settings.title': 'Configurações',
209
+ 'settings.company': 'Empresa',
210
+ 'settings.branding': 'Identidade Visual',
211
+ 'settings.security': 'Segurança',
212
+ 'settings.team': 'Equipe',
213
+ 'settings.billing': 'Faturamento',
214
+ 'settings.locations': 'Localizações',
215
+ 'billing.title': 'Planos e Faturamento',
216
+ 'billing.currentPlan': 'Plano Atual',
217
+ 'billing.upgrade': 'Fazer Upgrade',
218
+ 'billing.manage': 'Gerenciar Assinatura',
219
+ },
220
+ }
221
+
222
+ // Global translation registry — plugins merge their locales here so keys resolve
223
+ // regardless of which shell mounts the I18nProvider.
224
+ let _globalTranslations: Record<string, Record<string, string>> = {}
225
+
226
+ export function registerTranslations(translations?: Record<string, Record<string, string>>): void {
227
+ if (!translations) return
228
+ _globalTranslations = mergeTranslations(_globalTranslations, translations)
229
+ }
230
+
231
+ export function mergeTranslations(
232
+ base: Record<string, Record<string, string>>,
233
+ ...overrides: Array<Record<string, Record<string, string>> | undefined>
234
+ ): Record<string, Record<string, string>> {
235
+ const result = { ...base }
236
+ for (const override of overrides) {
237
+ if (!override) continue
238
+ for (const [locale, translations] of Object.entries(override)) {
239
+ result[locale] = { ...(result[locale] ?? {}), ...translations }
240
+ }
241
+ }
242
+ return result
243
+ }
244
+
245
+ // Seed the global fallback with core's own translations + the shared shell /
246
+ // CRUD / framework translations (extracted from saas-core's central i18n), so
247
+ // the native CRUD engine, framework UI and de-bridged plugins resolve their
248
+ // keys even under a host shell that does not mount @fayz-ai/core's I18nProvider.
249
+ registerTranslations(coreTranslations)
250
+ registerTranslations(shellTranslations)