@quazardous/qdadm 1.19.3

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 (291) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -0
  3. package/package.json +101 -0
  4. package/src/assets/logo.svg +6 -0
  5. package/src/auth/SessionAuthAdapter.ts +459 -0
  6. package/src/auth/index.ts +15 -0
  7. package/src/chain/ActiveStack.ts +154 -0
  8. package/src/chain/StackHydrator.ts +292 -0
  9. package/src/chain/index.ts +25 -0
  10. package/src/chain/useActiveStack.ts +120 -0
  11. package/src/chain/useStackHydrator.ts +155 -0
  12. package/src/components/BoolCell.vue +34 -0
  13. package/src/components/InfoBanner.vue +73 -0
  14. package/src/components/QdadmRoot.vue +52 -0
  15. package/src/components/SeverityTag.vue +86 -0
  16. package/src/components/dialogs/BulkStatusDialog.vue +60 -0
  17. package/src/components/dialogs/MultiStepDialog.vue +331 -0
  18. package/src/components/dialogs/SimpleDialog.vue +122 -0
  19. package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
  20. package/src/components/display/BannerZone.vue +65 -0
  21. package/src/components/display/CardsGrid.vue +72 -0
  22. package/src/components/display/CopyableId.vue +84 -0
  23. package/src/components/display/EmptyState.vue +62 -0
  24. package/src/components/display/IntensityBar.vue +190 -0
  25. package/src/components/display/RichCardsGrid.vue +101 -0
  26. package/src/components/edit/FormActions.vue +78 -0
  27. package/src/components/edit/FormField.vue +82 -0
  28. package/src/components/edit/FormInput.vue +158 -0
  29. package/src/components/edit/FormPage.vue +260 -0
  30. package/src/components/edit/FormTab.vue +217 -0
  31. package/src/components/edit/FormTabs.vue +120 -0
  32. package/src/components/edit/LookupField.vue +406 -0
  33. package/src/components/edit/LookupPickerDialog.vue +230 -0
  34. package/src/components/editors/JsonEditorFoldable.vue +470 -0
  35. package/src/components/editors/JsonStructuredField.vue +264 -0
  36. package/src/components/editors/JsonViewer.vue +51 -0
  37. package/src/components/editors/KeyValueEditor.vue +314 -0
  38. package/src/components/editors/LanguageEditor.vue +259 -0
  39. package/src/components/editors/PermissionEditor.vue +558 -0
  40. package/src/components/editors/ScopeEditor.vue +372 -0
  41. package/src/components/editors/VanillaJsonEditor.vue +183 -0
  42. package/src/components/index.ts +83 -0
  43. package/src/components/item/FieldGroups.vue +392 -0
  44. package/src/components/layout/AppLayout.vue +1032 -0
  45. package/src/components/layout/BaseLayout.vue +190 -0
  46. package/src/components/layout/Breadcrumb.vue +55 -0
  47. package/src/components/layout/DashboardLayout.vue +104 -0
  48. package/src/components/layout/FormLayout.vue +256 -0
  49. package/src/components/layout/ListLayout.vue +278 -0
  50. package/src/components/layout/PageHeader.vue +156 -0
  51. package/src/components/layout/PageLayout.vue +126 -0
  52. package/src/components/layout/PageNav.vue +192 -0
  53. package/src/components/layout/SidebarBox.vue +87 -0
  54. package/src/components/layout/Zone.vue +182 -0
  55. package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
  56. package/src/components/layout/defaults/DefaultFooter.vue +89 -0
  57. package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
  58. package/src/components/layout/defaults/DefaultHeader.vue +78 -0
  59. package/src/components/layout/defaults/DefaultMenu.vue +188 -0
  60. package/src/components/layout/defaults/DefaultPagination.vue +75 -0
  61. package/src/components/layout/defaults/DefaultTable.vue +137 -0
  62. package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
  63. package/src/components/layout/defaults/index.ts +16 -0
  64. package/src/components/lists/ActionButtons.vue +49 -0
  65. package/src/components/lists/ActionColumn.vue +40 -0
  66. package/src/components/lists/FilterBar.vue +54 -0
  67. package/src/components/lists/ListPage.vue +467 -0
  68. package/src/components/pages/LoginPage.vue +334 -0
  69. package/src/components/pages/NotFoundPage.vue +98 -0
  70. package/src/components/show/ShowDisplay.vue +343 -0
  71. package/src/components/show/ShowField.vue +67 -0
  72. package/src/components/show/ShowPage.vue +431 -0
  73. package/src/composables/index.ts +155 -0
  74. package/src/composables/useApp.ts +67 -0
  75. package/src/composables/useAuth.ts +98 -0
  76. package/src/composables/useBareForm.ts +319 -0
  77. package/src/composables/useBreadcrumb.ts +252 -0
  78. package/src/composables/useChildPage.ts +113 -0
  79. package/src/composables/useCurrentEntity.ts +60 -0
  80. package/src/composables/useDeferred.ts +94 -0
  81. package/src/composables/useDirtyState.ts +138 -0
  82. package/src/composables/useEntityItemFormPage.ts +942 -0
  83. package/src/composables/useEntityItemFormPage.types.ts +451 -0
  84. package/src/composables/useEntityItemPage.ts +438 -0
  85. package/src/composables/useEntityItemShowPage.ts +722 -0
  86. package/src/composables/useFieldManager.ts +692 -0
  87. package/src/composables/useForm.ts +544 -0
  88. package/src/composables/useGuardStore.ts +53 -0
  89. package/src/composables/useHooks.ts +56 -0
  90. package/src/composables/useInfoBanner.ts +217 -0
  91. package/src/composables/useJsonSyntax.ts +122 -0
  92. package/src/composables/useLayoutResolver.ts +329 -0
  93. package/src/composables/useListPage.ts +1535 -0
  94. package/src/composables/useListPage.types.ts +437 -0
  95. package/src/composables/useListPage.utils.ts +101 -0
  96. package/src/composables/useNavContext.ts +439 -0
  97. package/src/composables/useNavigation.ts +216 -0
  98. package/src/composables/useOptionsLookup.ts +323 -0
  99. package/src/composables/usePageTitle.ts +84 -0
  100. package/src/composables/useSSEBridge.ts +142 -0
  101. package/src/composables/useSemanticBreadcrumb.ts +222 -0
  102. package/src/composables/useSidebarState.ts +24 -0
  103. package/src/composables/useSignals.ts +52 -0
  104. package/src/composables/useUnsavedChangesGuard.ts +155 -0
  105. package/src/composables/useUserImpersonator.ts +317 -0
  106. package/src/composables/useZoneRegistry.ts +252 -0
  107. package/src/core/bundles.ts +558 -0
  108. package/src/core/decorator.ts +420 -0
  109. package/src/core/extension.ts +470 -0
  110. package/src/core/index.ts +59 -0
  111. package/src/deferred/DeferredRegistry.ts +353 -0
  112. package/src/deferred/index.ts +15 -0
  113. package/src/editors/index.ts +12 -0
  114. package/src/entity/EntityManager.cache.ts +530 -0
  115. package/src/entity/EntityManager.crud.ts +615 -0
  116. package/src/entity/EntityManager.interface.ts +78 -0
  117. package/src/entity/EntityManager.query.ts +221 -0
  118. package/src/entity/EntityManager.relations.ts +151 -0
  119. package/src/entity/EntityManager.ts +944 -0
  120. package/src/entity/EntityManager.types.ts +288 -0
  121. package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
  122. package/src/entity/auth/EntityAuthAdapter.ts +119 -0
  123. package/src/entity/auth/PermissiveAdapter.ts +43 -0
  124. package/src/entity/auth/RoleHierarchy.ts +128 -0
  125. package/src/entity/auth/SecurityChecker.ts +202 -0
  126. package/src/entity/auth/factory.ts +203 -0
  127. package/src/entity/auth/index.ts +47 -0
  128. package/src/entity/factory.test.js +189 -0
  129. package/src/entity/factory.ts +146 -0
  130. package/src/entity/index.ts +49 -0
  131. package/src/entity/storage/ApiStorage.ts +228 -0
  132. package/src/entity/storage/IStorage.ts +100 -0
  133. package/src/entity/storage/LocalStorage.ts +200 -0
  134. package/src/entity/storage/MemoryStorage.ts +184 -0
  135. package/src/entity/storage/MockApiStorage.ts +318 -0
  136. package/src/entity/storage/SdkStorage.ts +377 -0
  137. package/src/entity/storage/factory.test.js +159 -0
  138. package/src/entity/storage/factory.ts +209 -0
  139. package/src/entity/storage/index.ts +75 -0
  140. package/src/gen/FieldMapper.ts +122 -0
  141. package/src/gen/StorageProfileFactory.ts +93 -0
  142. package/src/gen/connectors/BaseConnector.ts +150 -0
  143. package/src/gen/connectors/ManualConnector.test.js +499 -0
  144. package/src/gen/connectors/ManualConnector.ts +414 -0
  145. package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
  146. package/src/gen/connectors/OpenAPIConnector.ts +693 -0
  147. package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
  148. package/src/gen/connectors/index.ts +27 -0
  149. package/src/gen/createManagers.ts +245 -0
  150. package/src/gen/decorators.ts +134 -0
  151. package/src/gen/generateManagers.test.js +540 -0
  152. package/src/gen/generateManagers.ts +398 -0
  153. package/src/gen/index.ts +66 -0
  154. package/src/gen/schema.ts +248 -0
  155. package/src/gen/vite-plugin.ts +114 -0
  156. package/src/generated/managers/testManager.js +45 -0
  157. package/src/generated/managers/testManager.ts +46 -0
  158. package/src/hooks/HookRegistry.ts +16 -0
  159. package/src/hooks/index.ts +15 -0
  160. package/src/i18n/I18n.ts +344 -0
  161. package/src/i18n/IncrementalDomainProvider.ts +153 -0
  162. package/src/i18n/InlineTranslationProvider.ts +4 -0
  163. package/src/i18n/LazyTranslationProvider.ts +102 -0
  164. package/src/i18n/MessagesRegistry.ts +4 -0
  165. package/src/i18n/Resolver.ts +4 -0
  166. package/src/i18n/__tests__/I18n.test.ts +169 -0
  167. package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
  168. package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
  169. package/src/i18n/__tests__/Resolver.test.ts +271 -0
  170. package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
  171. package/src/i18n/defaults/core.en.yml +55 -0
  172. package/src/i18n/defaults/core.fr.yml +55 -0
  173. package/src/i18n/index.ts +55 -0
  174. package/src/i18n/loaders/raw-modules.d.ts +15 -0
  175. package/src/i18n/loaders/yaml.ts +35 -0
  176. package/src/i18n/strategies.ts +4 -0
  177. package/src/i18n/types.ts +34 -0
  178. package/src/i18n/useI18n.ts +34 -0
  179. package/src/index.ts +453 -0
  180. package/src/kernel/EventRouter.ts +23 -0
  181. package/src/kernel/Kernel.api.ts +79 -0
  182. package/src/kernel/Kernel.auth.ts +120 -0
  183. package/src/kernel/Kernel.i18n.ts +29 -0
  184. package/src/kernel/Kernel.modules.ts +170 -0
  185. package/src/kernel/Kernel.registries.ts +237 -0
  186. package/src/kernel/Kernel.routing.ts +334 -0
  187. package/src/kernel/Kernel.ts +460 -0
  188. package/src/kernel/Kernel.types.ts +263 -0
  189. package/src/kernel/Kernel.vue.ts +304 -0
  190. package/src/kernel/KernelContext.entities.ts +80 -0
  191. package/src/kernel/KernelContext.events.ts +57 -0
  192. package/src/kernel/KernelContext.i18n.ts +37 -0
  193. package/src/kernel/KernelContext.permissions.ts +38 -0
  194. package/src/kernel/KernelContext.routing.ts +280 -0
  195. package/src/kernel/KernelContext.ts +252 -0
  196. package/src/kernel/KernelContext.types.ts +173 -0
  197. package/src/kernel/KernelContext.zones.ts +54 -0
  198. package/src/kernel/Module.ts +169 -0
  199. package/src/kernel/ModuleLoader.ts +645 -0
  200. package/src/kernel/SSEBridge.ts +11 -0
  201. package/src/kernel/SignalBus.ts +48 -0
  202. package/src/kernel/index.ts +56 -0
  203. package/src/module/index.ts +33 -0
  204. package/src/module/moduleRegistry.ts +492 -0
  205. package/src/modules/debug/AuthCollector.ts +501 -0
  206. package/src/modules/debug/Collector.ts +20 -0
  207. package/src/modules/debug/DebugBridge.ts +14 -0
  208. package/src/modules/debug/DebugModule.ts +220 -0
  209. package/src/modules/debug/EntitiesCollector.ts +839 -0
  210. package/src/modules/debug/ErrorCollector.ts +4 -0
  211. package/src/modules/debug/I18nCollector.ts +9 -0
  212. package/src/modules/debug/LocalStorageAdapter.ts +7 -0
  213. package/src/modules/debug/RouterCollector.ts +449 -0
  214. package/src/modules/debug/SignalCollector.ts +4 -0
  215. package/src/modules/debug/ToastCollector.ts +4 -0
  216. package/src/modules/debug/ZonesCollector.ts +447 -0
  217. package/src/modules/debug/components/DebugBar.vue +31 -0
  218. package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
  219. package/src/modules/debug/components/index.ts +9 -0
  220. package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
  221. package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
  222. package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
  223. package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
  224. package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
  225. package/src/modules/debug/components/panels/index.ts +16 -0
  226. package/src/modules/debug/index.ts +58 -0
  227. package/src/modules/debug/styles.scss +2671 -0
  228. package/src/notifications/NotificationBadge.vue +35 -0
  229. package/src/notifications/NotificationListener.vue +71 -0
  230. package/src/notifications/NotificationModule.ts +76 -0
  231. package/src/notifications/NotificationPanel.vue +478 -0
  232. package/src/notifications/NotificationStore.ts +288 -0
  233. package/src/notifications/index.ts +42 -0
  234. package/src/notifications/styles.scss +32 -0
  235. package/src/orchestrator/Orchestrator.ts +427 -0
  236. package/src/orchestrator/index.ts +8 -0
  237. package/src/orchestrator/useOrchestrator.ts +80 -0
  238. package/src/plugin.ts +251 -0
  239. package/src/query/FilterQuery.ts +306 -0
  240. package/src/query/QueryExecutor.ts +341 -0
  241. package/src/query/index.ts +16 -0
  242. package/src/security/EntityRolesProvider.ts +385 -0
  243. package/src/security/PermissionMatcher.ts +152 -0
  244. package/src/security/PermissionRegistry.ts +273 -0
  245. package/src/security/PersistableRolesProvider.ts +584 -0
  246. package/src/security/RolesManager.ts +136 -0
  247. package/src/security/RolesProvider.ts +167 -0
  248. package/src/security/RolesProviderStorage.ts +211 -0
  249. package/src/security/SecurityModule.ts +94 -0
  250. package/src/security/StaticRolesProvider.ts +117 -0
  251. package/src/security/UsersManager.ts +163 -0
  252. package/src/security/index.ts +77 -0
  253. package/src/security/pages/RoleForm.vue +276 -0
  254. package/src/security/pages/RoleList.vue +123 -0
  255. package/src/styles/_alerts.scss +95 -0
  256. package/src/styles/_buttons.scss +391 -0
  257. package/src/styles/_cards.scss +241 -0
  258. package/src/styles/_code.scss +78 -0
  259. package/src/styles/_dialogs.scss +55 -0
  260. package/src/styles/_filter-bar.scss +70 -0
  261. package/src/styles/_forms.scss +434 -0
  262. package/src/styles/_helpers.scss +153 -0
  263. package/src/styles/_lists.scss +225 -0
  264. package/src/styles/_main.scss +356 -0
  265. package/src/styles/_markdown.scss +88 -0
  266. package/src/styles/_responsive.scss +255 -0
  267. package/src/styles/_show-pages.scss +186 -0
  268. package/src/styles/_sidebar.scss +380 -0
  269. package/src/styles/_states.scss +115 -0
  270. package/src/styles/_stats.scss +47 -0
  271. package/src/styles/_variables.scss +224 -0
  272. package/src/styles/index.scss +38 -0
  273. package/src/styles/theme/_components.scss +331 -0
  274. package/src/styles/theme/_index.scss +10 -0
  275. package/src/styles/theme/_tokens.scss +147 -0
  276. package/src/styles/theme/_utilities.scss +175 -0
  277. package/src/toast/ToastBridgeModule.ts +68 -0
  278. package/src/toast/ToastListener.vue +55 -0
  279. package/src/toast/index.ts +26 -0
  280. package/src/toast/useSignalToast.ts +154 -0
  281. package/src/types/index.ts +285 -0
  282. package/src/types/pluralize.d.ts +19 -0
  283. package/src/types/quarkernel.d.ts +50 -0
  284. package/src/utils/formatters.ts +174 -0
  285. package/src/utils/index.ts +35 -0
  286. package/src/utils/transformers.ts +131 -0
  287. package/src/vite/qdadmDebugPlugin.ts +401 -0
  288. package/src/vite-env.d.ts +50 -0
  289. package/src/zones/ZoneRegistry.ts +934 -0
  290. package/src/zones/index.ts +29 -0
  291. package/src/zones/zones.ts +191 -0
@@ -0,0 +1,271 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ import { MessagesRegistry } from '../MessagesRegistry'
4
+ import { Resolver, snakeCaseToTitle } from '../Resolver'
5
+ import type { AliasPattern } from '../types'
6
+
7
+ function makeResolver(opts: {
8
+ bundles?: Record<string, Parameters<MessagesRegistry['merge']>[1]>
9
+ globalAliases?: AliasPattern[]
10
+ defaultLocale?: string
11
+ fallbackLocale?: string
12
+ onMissing?: (key: string, locale: string) => void
13
+ }) {
14
+ const reg = new MessagesRegistry()
15
+ for (const [locale, bundle] of Object.entries(opts.bundles ?? {})) {
16
+ reg.merge(locale, bundle)
17
+ }
18
+ const resolver = new Resolver(reg, {
19
+ defaultLocale: opts.defaultLocale ?? 'en',
20
+ fallbackLocale: opts.fallbackLocale ?? 'en',
21
+ globalAliases: opts.globalAliases,
22
+ onMissing: opts.onMissing,
23
+ })
24
+ return { reg, resolver }
25
+ }
26
+
27
+ describe('snakeCaseToTitle', () => {
28
+ it('handles snake_case', () => {
29
+ expect(snakeCaseToTitle('first_name')).toBe('First Name')
30
+ })
31
+ it('handles camelCase', () => {
32
+ expect(snakeCaseToTitle('firstName')).toBe('First Name')
33
+ })
34
+ it('handles kebab-case', () => {
35
+ expect(snakeCaseToTitle('first-name')).toBe('First Name')
36
+ })
37
+ it('handles single word', () => {
38
+ expect(snakeCaseToTitle('title')).toBe('Title')
39
+ })
40
+ })
41
+
42
+ describe('Resolver — convention lookup', () => {
43
+ it('returns the matching value from the active locale', () => {
44
+ const { resolver } = makeResolver({
45
+ bundles: { en: { entities: { books: { fields: { title: 'Title' } } } } },
46
+ })
47
+ expect(resolver.translate('entities.books.fields.title', 'en')).toBe('Title')
48
+ })
49
+
50
+ it('falls back to fallbackLocale on miss', () => {
51
+ const { resolver } = makeResolver({
52
+ bundles: { en: { entities: { books: { fields: { title: 'Title' } } } } },
53
+ defaultLocale: 'fr',
54
+ fallbackLocale: 'en',
55
+ })
56
+ // No 'fr' bundle — falls back to 'en'
57
+ expect(resolver.translate('entities.books.fields.title', 'fr')).toBe('Title')
58
+ })
59
+
60
+ it('reads _label from object node', () => {
61
+ const { resolver } = makeResolver({
62
+ bundles: {
63
+ en: {
64
+ entities: {
65
+ books: {
66
+ fields: {
67
+ genre: { _label: 'Genre', options: { fiction: 'Fiction' } },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ })
74
+ expect(resolver.translate('entities.books.fields.genre', 'en')).toBe('Genre')
75
+ expect(
76
+ resolver.translate('entities.books.fields.genre.options.fiction', 'en')
77
+ ).toBe('Fiction')
78
+ })
79
+ })
80
+
81
+ describe('Resolver — interpolation', () => {
82
+ it('replaces {placeholder} params', () => {
83
+ const { resolver } = makeResolver({
84
+ bundles: { en: { core: { errors: { tooShort: 'Too short (min {min})' } } } },
85
+ })
86
+ expect(resolver.translate('core.errors.tooShort', 'en', { min: 3 })).toBe(
87
+ 'Too short (min 3)'
88
+ )
89
+ })
90
+
91
+ it('leaves unknown placeholders untouched', () => {
92
+ const { resolver } = makeResolver({
93
+ bundles: { en: { foo: 'Hello {name}' } },
94
+ })
95
+ expect(resolver.translate('foo', 'en')).toBe('Hello {name}')
96
+ })
97
+ })
98
+
99
+ describe('Resolver — value-level @-aliases', () => {
100
+ it('follows a single @-alias', () => {
101
+ const { resolver } = makeResolver({
102
+ bundles: {
103
+ en: {
104
+ entities: { books: { actions: { save: '@core.actions.save' } } },
105
+ core: { actions: { save: 'Save' } },
106
+ },
107
+ },
108
+ })
109
+ expect(resolver.translate('entities.books.actions.save', 'en')).toBe('Save')
110
+ })
111
+
112
+ it('follows a chain of @-aliases', () => {
113
+ const { resolver } = makeResolver({
114
+ bundles: {
115
+ en: {
116
+ a: '@b',
117
+ b: '@c',
118
+ c: 'Bottom',
119
+ },
120
+ },
121
+ })
122
+ expect(resolver.translate('a', 'en')).toBe('Bottom')
123
+ })
124
+
125
+ it('detects cycles and falls through', () => {
126
+ const onMissing = vitestFn()
127
+ const { resolver } = makeResolver({
128
+ bundles: { en: { a: '@b', b: '@a' } },
129
+ onMissing,
130
+ })
131
+ // No final string → ends up as miss → returns raw key
132
+ expect(resolver.translate('a', 'en')).toBe('a')
133
+ expect(onMissing.calls.length).toBeGreaterThan(0)
134
+ })
135
+
136
+ it('escapes literal @ via @@ prefix', () => {
137
+ const { resolver } = makeResolver({
138
+ bundles: { en: { ping: '@@home' } },
139
+ })
140
+ expect(resolver.translate('ping', 'en')).toBe('@home')
141
+ })
142
+ })
143
+
144
+ describe('Resolver — pattern aliases', () => {
145
+ const globalAliases: AliasPattern[] = [
146
+ { pattern: 'entities.*.actions.*', target: 'core.actions.$2' },
147
+ { pattern: 'entities.*.fields.created_at', target: 'core.fields.created_at' },
148
+ ]
149
+
150
+ it('rewrites via wildcard pattern with $2 capture', () => {
151
+ const { resolver } = makeResolver({
152
+ bundles: { en: { core: { actions: { save: 'Save' } } } },
153
+ globalAliases,
154
+ })
155
+ expect(resolver.translate('entities.books.actions.save', 'en')).toBe('Save')
156
+ })
157
+
158
+ it('rewrites a specific exact pattern', () => {
159
+ const { resolver } = makeResolver({
160
+ bundles: { en: { core: { fields: { created_at: 'Created at' } } } },
161
+ globalAliases,
162
+ })
163
+ expect(resolver.translate('entities.books.fields.created_at', 'en')).toBe('Created at')
164
+ })
165
+
166
+ it('longest-match wins when multiple patterns match', () => {
167
+ const aliases: AliasPattern[] = [
168
+ { pattern: 'entities.*.actions.*', target: 'core.actions.$2' },
169
+ // More specific, should win
170
+ { pattern: 'entities.books.actions.delete', target: 'books.specialDelete' },
171
+ ]
172
+ const { resolver } = makeResolver({
173
+ bundles: {
174
+ en: {
175
+ core: { actions: { delete: 'Delete' } },
176
+ books: { specialDelete: 'Discard' },
177
+ },
178
+ },
179
+ globalAliases: aliases,
180
+ })
181
+ expect(resolver.translate('entities.books.actions.delete', 'en')).toBe('Discard')
182
+ // Other entities still go through the wildcard
183
+ expect(resolver.translate('entities.users.actions.delete', 'en')).toBe('Delete')
184
+ })
185
+
186
+ it('does not loop forever when patterns ping-pong', () => {
187
+ const aliases: AliasPattern[] = [
188
+ { pattern: 'a.*', target: 'b.$1' },
189
+ { pattern: 'b.*', target: 'a.$1' },
190
+ ]
191
+ const { resolver } = makeResolver({
192
+ bundles: { en: {} },
193
+ globalAliases: aliases,
194
+ })
195
+ // Should bail out — no actual value found anywhere
196
+ expect(resolver.translate('a.foo', 'en')).toBe('a.foo')
197
+ })
198
+ })
199
+
200
+ describe('Resolver — final fallbacks', () => {
201
+ it('humanizes field key via snakeCaseToTitle on miss', () => {
202
+ const { resolver } = makeResolver({ bundles: { en: {} } })
203
+ expect(resolver.translate('entities.books.fields.first_name', 'en')).toBe('First Name')
204
+ })
205
+
206
+ it('does NOT humanize non-field keys', () => {
207
+ const { resolver } = makeResolver({ bundles: { en: {} } })
208
+ expect(resolver.translate('core.actions.save', 'en')).toBe('core.actions.save')
209
+ })
210
+
211
+ it('emits onMissing on every miss', () => {
212
+ const onMissing = vitestFn<[string, string]>()
213
+ const { resolver } = makeResolver({ bundles: { en: {} }, onMissing })
214
+ resolver.translate('core.actions.save', 'en')
215
+ resolver.translate('entities.books.fields.title', 'en') // miss but humanized
216
+ expect(onMissing.calls.length).toBe(2)
217
+ expect(onMissing.calls[0]).toEqual(['core.actions.save', 'en'])
218
+ expect(onMissing.calls[1]).toEqual(['entities.books.fields.title', 'en'])
219
+ })
220
+ })
221
+
222
+ describe('Resolver — trace', () => {
223
+ it('returns lookup steps', () => {
224
+ const { resolver } = makeResolver({
225
+ bundles: { en: { entities: { books: { fields: { title: 'Title' } } } } },
226
+ })
227
+ const trace = resolver.resolve('entities.books.fields.title', 'en')
228
+ expect(trace.hit).toBe(true)
229
+ expect(trace.result).toBe('Title')
230
+ expect(trace.steps[0]).toEqual({
231
+ kind: 'lookup',
232
+ locale: 'en',
233
+ key: 'entities.books.fields.title',
234
+ found: true,
235
+ })
236
+ })
237
+
238
+ it('records the alias-pattern step on rewrite', () => {
239
+ const { resolver } = makeResolver({
240
+ bundles: { en: { core: { actions: { save: 'Save' } } } },
241
+ globalAliases: [{ pattern: 'entities.*.actions.*', target: 'core.actions.$2' }],
242
+ })
243
+ const trace = resolver.resolve('entities.books.actions.save', 'en')
244
+ expect(trace.hit).toBe(true)
245
+ const aliasStep = trace.steps.find((s) => s.kind === 'alias-pattern')
246
+ expect(aliasStep).toBeDefined()
247
+ })
248
+
249
+ it('records snake-case-fallback for missing field', () => {
250
+ const { resolver } = makeResolver({ bundles: { en: {} } })
251
+ const trace = resolver.resolve('entities.books.fields.first_name', 'en')
252
+ expect(trace.hit).toBe(false)
253
+ expect(trace.result).toBe('First Name')
254
+ const last = trace.steps[trace.steps.length - 1]
255
+ expect(last?.kind).toBe('snake-case-fallback')
256
+ })
257
+ })
258
+
259
+ // ----------------------------------------------------------------------------
260
+ // Tiny mock helper (avoids import-order quirks with vi.fn() in some setups)
261
+ // ----------------------------------------------------------------------------
262
+ function vitestFn<A extends unknown[] = unknown[]>(): ((...args: A) => void) & {
263
+ calls: A[]
264
+ } {
265
+ const calls: A[] = []
266
+ const fn = ((...args: A) => {
267
+ calls.push(args)
268
+ }) as ((...args: A) => void) & { calls: A[] }
269
+ fn.calls = calls
270
+ return fn
271
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Default core provider — ships qdadm's `core.*` defaults (EN + FR) as a
3
+ * `LazyTranslationProvider` configured with a YAML loader.
4
+ *
5
+ * The YAML files (`core.en.yml`, `core.fr.yml`) are imported via Vite's
6
+ * `?raw` query so the text is fetched on-demand, then parsed at runtime.
7
+ * Splitting the bundle by locale (and, in the future, by domain) keeps the
8
+ * provider tree-shakeable and translator-friendly.
9
+ *
10
+ * Adding a built-in locale = drop `core.<locale>.yml` next to this file and
11
+ * add it to the loader map below.
12
+ */
13
+
14
+ import { LazyTranslationProvider } from '../LazyTranslationProvider'
15
+ import { createYamlLoader } from '../loaders/yaml'
16
+
17
+ export function createDefaultCoreProvider(): LazyTranslationProvider {
18
+ const yamlLoader = createYamlLoader({
19
+ en: () => import('./core.en.yml?raw'),
20
+ fr: () => import('./core.fr.yml?raw'),
21
+ })
22
+
23
+ return new LazyTranslationProvider({
24
+ name: 'default-core',
25
+ loaders: [yamlLoader],
26
+ locales: ['en', 'fr'],
27
+ })
28
+ }
@@ -0,0 +1,55 @@
1
+ core:
2
+ actions:
3
+ save: Save
4
+ cancel: Cancel
5
+ delete: Delete
6
+ edit: Edit
7
+ create: Create
8
+ update: Update
9
+ createAndClose: Create & Close
10
+ updateAndClose: Update & Close
11
+ add: Add
12
+ remove: Remove
13
+ submit: Submit
14
+ reset: Reset
15
+ back: Back
16
+ next: Next
17
+ previous: Previous
18
+ close: Close
19
+ confirm: Confirm
20
+ search: Search
21
+ filter: Filter
22
+ clear: Clear
23
+ refresh: Refresh
24
+ export: Export
25
+ import: Import
26
+ tooltips:
27
+ saveAndContinue: Save and continue editing
28
+ saveAndReturn: Save and return to list
29
+ unsavedChanges: Unsaved changes
30
+ placeholders:
31
+ search: Search…
32
+ noSelection: Click to select…
33
+ fields:
34
+ id: ID
35
+ created_at: Created at
36
+ updated_at: Updated at
37
+ created_by: Created by
38
+ updated_by: Updated by
39
+ messages:
40
+ empty: No items available
41
+ loading: Loading…
42
+ noResults: No results
43
+ noMatching: No matching items
44
+ saved: Saved
45
+ deleted: Deleted
46
+ created: Created
47
+ updated: Updated
48
+ confirmDelete: Are you sure you want to delete this item?
49
+ errors:
50
+ required: Required
51
+ tooShort: Too short (min {min})
52
+ tooLong: Too long (max {max})
53
+ invalid: Invalid value
54
+ notFound: Not found
55
+ unknown: An unknown error occurred
@@ -0,0 +1,55 @@
1
+ core:
2
+ actions:
3
+ save: Enregistrer
4
+ cancel: Annuler
5
+ delete: Supprimer
6
+ edit: Modifier
7
+ create: Créer
8
+ update: Mettre à jour
9
+ createAndClose: Créer et fermer
10
+ updateAndClose: Mettre à jour et fermer
11
+ add: Ajouter
12
+ remove: Retirer
13
+ submit: Valider
14
+ reset: Réinitialiser
15
+ back: Retour
16
+ next: Suivant
17
+ previous: Précédent
18
+ close: Fermer
19
+ confirm: Confirmer
20
+ search: Rechercher
21
+ filter: Filtrer
22
+ clear: Effacer
23
+ refresh: Actualiser
24
+ export: Exporter
25
+ import: Importer
26
+ tooltips:
27
+ saveAndContinue: Enregistrer et continuer la modification
28
+ saveAndReturn: Enregistrer et revenir à la liste
29
+ unsavedChanges: Modifications non enregistrées
30
+ placeholders:
31
+ search: Rechercher…
32
+ noSelection: Cliquez pour sélectionner…
33
+ fields:
34
+ id: Identifiant
35
+ created_at: Créé le
36
+ updated_at: Modifié le
37
+ created_by: Créé par
38
+ updated_by: Modifié par
39
+ messages:
40
+ empty: Aucun élément
41
+ loading: Chargement…
42
+ noResults: Aucun résultat
43
+ noMatching: Aucune correspondance
44
+ saved: Enregistré
45
+ deleted: Supprimé
46
+ created: Créé
47
+ updated: Modifié
48
+ confirmDelete: Êtes-vous sûr de vouloir supprimer cet élément ?
49
+ errors:
50
+ required: Requis
51
+ tooShort: Trop court (min {min})
52
+ tooLong: Trop long (max {max})
53
+ invalid: Valeur invalide
54
+ notFound: Introuvable
55
+ unknown: Une erreur inconnue est survenue
@@ -0,0 +1,55 @@
1
+ /**
2
+ * qdadm i18n - public exports.
3
+ *
4
+ * ## Two providers to choose from
5
+ *
6
+ * - **`LazyTranslationProvider`** — full bundle on `load(locale)`, split
7
+ * across multiple loaders (one per file/domain) deep-merged on resolve.
8
+ * Use when the locale bundle is reasonable to fetch up-front.
9
+ *
10
+ * - **`IncrementalDomainProvider`** — one loader per top-level domain,
11
+ * fetched on demand. `t()` on an unloaded domain returns the raw key
12
+ * synchronously and emits `i18n:domain-loaded` once merged. Use for
13
+ * large apps with rarely-used sections.
14
+ *
15
+ * Both support `createYamlLoader({ <locale>: () => import('./xx.yml?raw') })`
16
+ * for the typical "split YAML files in a directory" setup.
17
+ *
18
+ * See docs/todo-i18n.md for the broader design rationale.
19
+ */
20
+
21
+ export { I18n } from './I18n'
22
+ export type { I18nDeps } from './I18n'
23
+ export { MessagesRegistry } from './MessagesRegistry'
24
+ export { Resolver, snakeCaseToTitle } from './Resolver'
25
+ export { InlineTranslationProvider } from './InlineTranslationProvider'
26
+ export { defineStrategy, resolveStrategy, STRATEGIES } from './strategies'
27
+ export { LazyTranslationProvider } from './LazyTranslationProvider'
28
+ export type { LazyLoader, LazyTranslationProviderOptions } from './LazyTranslationProvider'
29
+ export {
30
+ IncrementalDomainProvider,
31
+ isDomainAwareProvider,
32
+ } from './IncrementalDomainProvider'
33
+ export type {
34
+ DomainLoader,
35
+ IncrementalDomainProviderOptions,
36
+ } from './IncrementalDomainProvider'
37
+ export { createYamlLoader } from './loaders/yaml'
38
+ export type { YamlTextImport } from './loaders/yaml'
39
+ export { createDefaultCoreProvider } from './defaults/DefaultCoreProvider'
40
+ export { useI18n, I18N_INJECTION_KEY } from './useI18n'
41
+ export type {
42
+ AliasPattern,
43
+ CoreMessages,
44
+ EntityMessages,
45
+ I18nOptions,
46
+ KeyStrategy,
47
+ KeyStrategyName,
48
+ MessagesBundle,
49
+ MessagesNode,
50
+ NavMessages,
51
+ ResolveStep,
52
+ ResolveTrace,
53
+ TranslateParams,
54
+ TranslationProvider,
55
+ } from './types'
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ambient declarations for Vite/Vitest `?raw` imports of arbitrary text
3
+ * files (YAML, etc.). Lets `import yamlText from './foo.yml?raw'` type-check
4
+ * without per-call casts.
5
+ */
6
+
7
+ declare module '*.yml?raw' {
8
+ const content: string
9
+ export default content
10
+ }
11
+
12
+ declare module '*.yaml?raw' {
13
+ const content: string
14
+ export default content
15
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * YAML loader factory for LazyTranslationProvider.
3
+ *
4
+ * Given a `locale → () => Promise<{ default: string }>` map, returns a
5
+ * `LazyLoader` that resolves the YAML text for the requested locale and
6
+ * parses it through the `yaml` package.
7
+ *
8
+ * Typical usage with Vite/Vitest `?raw` imports (qdadm-internal default):
9
+ *
10
+ * const loader = createYamlLoader({
11
+ * en: () => import('./core.en.yml?raw'),
12
+ * fr: () => import('./core.fr.yml?raw'),
13
+ * })
14
+ *
15
+ * The dynamic-import boundary is what gives us code-splitting: the YAML for
16
+ * a locale is only fetched + parsed when that locale is requested.
17
+ */
18
+
19
+ import { parse as parseYaml } from 'yaml'
20
+
21
+ import type { MessagesBundle } from '@quazardous/qdcore'
22
+ import type { LazyLoader } from '../LazyTranslationProvider'
23
+
24
+ export type YamlTextImport = () => Promise<{ default: string }>
25
+
26
+ export function createYamlLoader(textImports: Record<string, YamlTextImport>): LazyLoader {
27
+ return async (locale: string) => {
28
+ const importer = textImports[locale]
29
+ if (!importer) return null
30
+ const mod = await importer()
31
+ const parsed = parseYaml(mod.default)
32
+ if (!parsed || typeof parsed !== 'object') return null
33
+ return parsed as MessagesBundle
34
+ }
35
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * strategies — qdadm re-export from `@quazardous/qdcore/i18n`.
3
+ */
4
+ export { STRATEGIES, resolveStrategy, defineStrategy } from '@quazardous/qdcore'
@@ -0,0 +1,34 @@
1
+ /**
2
+ * qdadm i18n types.
3
+ *
4
+ * Generic types come from `@quazardous/qdcore/i18n`; this module adds the
5
+ * admin-specific `I18nOptions` (kernel constructor option that toggles the
6
+ * default core admin bundles shipped via DefaultCoreProvider).
7
+ */
8
+
9
+ import type { BaseI18nOptions } from '@quazardous/qdcore'
10
+
11
+ export type {
12
+ AliasPattern,
13
+ BaseI18nOptions,
14
+ CoreMessages,
15
+ EntityMessages,
16
+ KeyStrategy,
17
+ KeyStrategyName,
18
+ MessagesBundle,
19
+ MessagesNode,
20
+ NavMessages,
21
+ ResolveStep,
22
+ ResolveTrace,
23
+ TranslateParams,
24
+ TranslationProvider,
25
+ } from '@quazardous/qdcore'
26
+
27
+ /**
28
+ * Kernel-level i18n options for qdadm. Extends the generic shared shape with
29
+ * admin-only knobs.
30
+ */
31
+ export interface I18nOptions extends BaseI18nOptions {
32
+ /** Disable shipping qdadm's default core.* bundles (EN + FR). Default: false. */
33
+ disableDefaultCoreBundle?: boolean
34
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * useI18n - Vue composable for accessing the kernel I18n instance.
3
+ *
4
+ * Falls back to a no-op shim if no I18n has been provided to the app — useful
5
+ * for components rendered outside a kernel (tests, isolated examples).
6
+ */
7
+
8
+ import { computed, inject, type Ref, type ComputedRef } from 'vue'
9
+ import type { I18n } from './I18n'
10
+ import type { TranslateParams } from './types'
11
+
12
+ export const I18N_INJECTION_KEY = Symbol('qdadm.i18n')
13
+
14
+ export interface UseI18nResult {
15
+ t: (key: string, params?: TranslateParams) => string
16
+ locale: ComputedRef<string> | Ref<string>
17
+ i18n: I18n | null
18
+ }
19
+
20
+ export function useI18n(): UseI18nResult {
21
+ const i18n = inject<I18n | null>(I18N_INJECTION_KEY, null)
22
+ if (!i18n) {
23
+ return {
24
+ t: (key: string) => key,
25
+ locale: computed(() => 'en'),
26
+ i18n: null,
27
+ }
28
+ }
29
+ return {
30
+ t: (key, params) => i18n.t(key, params),
31
+ locale: i18n.locale,
32
+ i18n,
33
+ }
34
+ }