@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.
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/package.json +101 -0
- package/src/assets/logo.svg +6 -0
- package/src/auth/SessionAuthAdapter.ts +459 -0
- package/src/auth/index.ts +15 -0
- package/src/chain/ActiveStack.ts +154 -0
- package/src/chain/StackHydrator.ts +292 -0
- package/src/chain/index.ts +25 -0
- package/src/chain/useActiveStack.ts +120 -0
- package/src/chain/useStackHydrator.ts +155 -0
- package/src/components/BoolCell.vue +34 -0
- package/src/components/InfoBanner.vue +73 -0
- package/src/components/QdadmRoot.vue +52 -0
- package/src/components/SeverityTag.vue +86 -0
- package/src/components/dialogs/BulkStatusDialog.vue +60 -0
- package/src/components/dialogs/MultiStepDialog.vue +331 -0
- package/src/components/dialogs/SimpleDialog.vue +122 -0
- package/src/components/dialogs/UnsavedChangesDialog.vue +80 -0
- package/src/components/display/BannerZone.vue +65 -0
- package/src/components/display/CardsGrid.vue +72 -0
- package/src/components/display/CopyableId.vue +84 -0
- package/src/components/display/EmptyState.vue +62 -0
- package/src/components/display/IntensityBar.vue +190 -0
- package/src/components/display/RichCardsGrid.vue +101 -0
- package/src/components/edit/FormActions.vue +78 -0
- package/src/components/edit/FormField.vue +82 -0
- package/src/components/edit/FormInput.vue +158 -0
- package/src/components/edit/FormPage.vue +260 -0
- package/src/components/edit/FormTab.vue +217 -0
- package/src/components/edit/FormTabs.vue +120 -0
- package/src/components/edit/LookupField.vue +406 -0
- package/src/components/edit/LookupPickerDialog.vue +230 -0
- package/src/components/editors/JsonEditorFoldable.vue +470 -0
- package/src/components/editors/JsonStructuredField.vue +264 -0
- package/src/components/editors/JsonViewer.vue +51 -0
- package/src/components/editors/KeyValueEditor.vue +314 -0
- package/src/components/editors/LanguageEditor.vue +259 -0
- package/src/components/editors/PermissionEditor.vue +558 -0
- package/src/components/editors/ScopeEditor.vue +372 -0
- package/src/components/editors/VanillaJsonEditor.vue +183 -0
- package/src/components/index.ts +83 -0
- package/src/components/item/FieldGroups.vue +392 -0
- package/src/components/layout/AppLayout.vue +1032 -0
- package/src/components/layout/BaseLayout.vue +190 -0
- package/src/components/layout/Breadcrumb.vue +55 -0
- package/src/components/layout/DashboardLayout.vue +104 -0
- package/src/components/layout/FormLayout.vue +256 -0
- package/src/components/layout/ListLayout.vue +278 -0
- package/src/components/layout/PageHeader.vue +156 -0
- package/src/components/layout/PageLayout.vue +126 -0
- package/src/components/layout/PageNav.vue +192 -0
- package/src/components/layout/SidebarBox.vue +87 -0
- package/src/components/layout/Zone.vue +182 -0
- package/src/components/layout/defaults/DefaultBreadcrumb.vue +149 -0
- package/src/components/layout/defaults/DefaultFooter.vue +89 -0
- package/src/components/layout/defaults/DefaultFormActions.vue +67 -0
- package/src/components/layout/defaults/DefaultHeader.vue +78 -0
- package/src/components/layout/defaults/DefaultMenu.vue +188 -0
- package/src/components/layout/defaults/DefaultPagination.vue +75 -0
- package/src/components/layout/defaults/DefaultTable.vue +137 -0
- package/src/components/layout/defaults/DefaultUserInfo.vue +111 -0
- package/src/components/layout/defaults/index.ts +16 -0
- package/src/components/lists/ActionButtons.vue +49 -0
- package/src/components/lists/ActionColumn.vue +40 -0
- package/src/components/lists/FilterBar.vue +54 -0
- package/src/components/lists/ListPage.vue +467 -0
- package/src/components/pages/LoginPage.vue +334 -0
- package/src/components/pages/NotFoundPage.vue +98 -0
- package/src/components/show/ShowDisplay.vue +343 -0
- package/src/components/show/ShowField.vue +67 -0
- package/src/components/show/ShowPage.vue +431 -0
- package/src/composables/index.ts +155 -0
- package/src/composables/useApp.ts +67 -0
- package/src/composables/useAuth.ts +98 -0
- package/src/composables/useBareForm.ts +319 -0
- package/src/composables/useBreadcrumb.ts +252 -0
- package/src/composables/useChildPage.ts +113 -0
- package/src/composables/useCurrentEntity.ts +60 -0
- package/src/composables/useDeferred.ts +94 -0
- package/src/composables/useDirtyState.ts +138 -0
- package/src/composables/useEntityItemFormPage.ts +942 -0
- package/src/composables/useEntityItemFormPage.types.ts +451 -0
- package/src/composables/useEntityItemPage.ts +438 -0
- package/src/composables/useEntityItemShowPage.ts +722 -0
- package/src/composables/useFieldManager.ts +692 -0
- package/src/composables/useForm.ts +544 -0
- package/src/composables/useGuardStore.ts +53 -0
- package/src/composables/useHooks.ts +56 -0
- package/src/composables/useInfoBanner.ts +217 -0
- package/src/composables/useJsonSyntax.ts +122 -0
- package/src/composables/useLayoutResolver.ts +329 -0
- package/src/composables/useListPage.ts +1535 -0
- package/src/composables/useListPage.types.ts +437 -0
- package/src/composables/useListPage.utils.ts +101 -0
- package/src/composables/useNavContext.ts +439 -0
- package/src/composables/useNavigation.ts +216 -0
- package/src/composables/useOptionsLookup.ts +323 -0
- package/src/composables/usePageTitle.ts +84 -0
- package/src/composables/useSSEBridge.ts +142 -0
- package/src/composables/useSemanticBreadcrumb.ts +222 -0
- package/src/composables/useSidebarState.ts +24 -0
- package/src/composables/useSignals.ts +52 -0
- package/src/composables/useUnsavedChangesGuard.ts +155 -0
- package/src/composables/useUserImpersonator.ts +317 -0
- package/src/composables/useZoneRegistry.ts +252 -0
- package/src/core/bundles.ts +558 -0
- package/src/core/decorator.ts +420 -0
- package/src/core/extension.ts +470 -0
- package/src/core/index.ts +59 -0
- package/src/deferred/DeferredRegistry.ts +353 -0
- package/src/deferred/index.ts +15 -0
- package/src/editors/index.ts +12 -0
- package/src/entity/EntityManager.cache.ts +530 -0
- package/src/entity/EntityManager.crud.ts +615 -0
- package/src/entity/EntityManager.interface.ts +78 -0
- package/src/entity/EntityManager.query.ts +221 -0
- package/src/entity/EntityManager.relations.ts +151 -0
- package/src/entity/EntityManager.ts +944 -0
- package/src/entity/EntityManager.types.ts +288 -0
- package/src/entity/auth/CompositeAuthAdapter.ts +194 -0
- package/src/entity/auth/EntityAuthAdapter.ts +119 -0
- package/src/entity/auth/PermissiveAdapter.ts +43 -0
- package/src/entity/auth/RoleHierarchy.ts +128 -0
- package/src/entity/auth/SecurityChecker.ts +202 -0
- package/src/entity/auth/factory.ts +203 -0
- package/src/entity/auth/index.ts +47 -0
- package/src/entity/factory.test.js +189 -0
- package/src/entity/factory.ts +146 -0
- package/src/entity/index.ts +49 -0
- package/src/entity/storage/ApiStorage.ts +228 -0
- package/src/entity/storage/IStorage.ts +100 -0
- package/src/entity/storage/LocalStorage.ts +200 -0
- package/src/entity/storage/MemoryStorage.ts +184 -0
- package/src/entity/storage/MockApiStorage.ts +318 -0
- package/src/entity/storage/SdkStorage.ts +377 -0
- package/src/entity/storage/factory.test.js +159 -0
- package/src/entity/storage/factory.ts +209 -0
- package/src/entity/storage/index.ts +75 -0
- package/src/gen/FieldMapper.ts +122 -0
- package/src/gen/StorageProfileFactory.ts +93 -0
- package/src/gen/connectors/BaseConnector.ts +150 -0
- package/src/gen/connectors/ManualConnector.test.js +499 -0
- package/src/gen/connectors/ManualConnector.ts +414 -0
- package/src/gen/connectors/OpenAPIConnector.test.js +836 -0
- package/src/gen/connectors/OpenAPIConnector.ts +693 -0
- package/src/gen/connectors/__fixtures__/sample-openapi.json +311 -0
- package/src/gen/connectors/index.ts +27 -0
- package/src/gen/createManagers.ts +245 -0
- package/src/gen/decorators.ts +134 -0
- package/src/gen/generateManagers.test.js +540 -0
- package/src/gen/generateManagers.ts +398 -0
- package/src/gen/index.ts +66 -0
- package/src/gen/schema.ts +248 -0
- package/src/gen/vite-plugin.ts +114 -0
- package/src/generated/managers/testManager.js +45 -0
- package/src/generated/managers/testManager.ts +46 -0
- package/src/hooks/HookRegistry.ts +16 -0
- package/src/hooks/index.ts +15 -0
- package/src/i18n/I18n.ts +344 -0
- package/src/i18n/IncrementalDomainProvider.ts +153 -0
- package/src/i18n/InlineTranslationProvider.ts +4 -0
- package/src/i18n/LazyTranslationProvider.ts +102 -0
- package/src/i18n/MessagesRegistry.ts +4 -0
- package/src/i18n/Resolver.ts +4 -0
- package/src/i18n/__tests__/I18n.test.ts +169 -0
- package/src/i18n/__tests__/IncrementalDomainProvider.test.ts +146 -0
- package/src/i18n/__tests__/LazyTranslationProvider.test.ts +100 -0
- package/src/i18n/__tests__/Resolver.test.ts +271 -0
- package/src/i18n/defaults/DefaultCoreProvider.ts +28 -0
- package/src/i18n/defaults/core.en.yml +55 -0
- package/src/i18n/defaults/core.fr.yml +55 -0
- package/src/i18n/index.ts +55 -0
- package/src/i18n/loaders/raw-modules.d.ts +15 -0
- package/src/i18n/loaders/yaml.ts +35 -0
- package/src/i18n/strategies.ts +4 -0
- package/src/i18n/types.ts +34 -0
- package/src/i18n/useI18n.ts +34 -0
- package/src/index.ts +453 -0
- package/src/kernel/EventRouter.ts +23 -0
- package/src/kernel/Kernel.api.ts +79 -0
- package/src/kernel/Kernel.auth.ts +120 -0
- package/src/kernel/Kernel.i18n.ts +29 -0
- package/src/kernel/Kernel.modules.ts +170 -0
- package/src/kernel/Kernel.registries.ts +237 -0
- package/src/kernel/Kernel.routing.ts +334 -0
- package/src/kernel/Kernel.ts +460 -0
- package/src/kernel/Kernel.types.ts +263 -0
- package/src/kernel/Kernel.vue.ts +304 -0
- package/src/kernel/KernelContext.entities.ts +80 -0
- package/src/kernel/KernelContext.events.ts +57 -0
- package/src/kernel/KernelContext.i18n.ts +37 -0
- package/src/kernel/KernelContext.permissions.ts +38 -0
- package/src/kernel/KernelContext.routing.ts +280 -0
- package/src/kernel/KernelContext.ts +252 -0
- package/src/kernel/KernelContext.types.ts +173 -0
- package/src/kernel/KernelContext.zones.ts +54 -0
- package/src/kernel/Module.ts +169 -0
- package/src/kernel/ModuleLoader.ts +645 -0
- package/src/kernel/SSEBridge.ts +11 -0
- package/src/kernel/SignalBus.ts +48 -0
- package/src/kernel/index.ts +56 -0
- package/src/module/index.ts +33 -0
- package/src/module/moduleRegistry.ts +492 -0
- package/src/modules/debug/AuthCollector.ts +501 -0
- package/src/modules/debug/Collector.ts +20 -0
- package/src/modules/debug/DebugBridge.ts +14 -0
- package/src/modules/debug/DebugModule.ts +220 -0
- package/src/modules/debug/EntitiesCollector.ts +839 -0
- package/src/modules/debug/ErrorCollector.ts +4 -0
- package/src/modules/debug/I18nCollector.ts +9 -0
- package/src/modules/debug/LocalStorageAdapter.ts +7 -0
- package/src/modules/debug/RouterCollector.ts +449 -0
- package/src/modules/debug/SignalCollector.ts +4 -0
- package/src/modules/debug/ToastCollector.ts +4 -0
- package/src/modules/debug/ZonesCollector.ts +447 -0
- package/src/modules/debug/components/DebugBar.vue +31 -0
- package/src/modules/debug/components/adminPanelsConfig.ts +42 -0
- package/src/modules/debug/components/index.ts +9 -0
- package/src/modules/debug/components/panels/AuthPanel.vue +267 -0
- package/src/modules/debug/components/panels/EntitiesPanel.vue +572 -0
- package/src/modules/debug/components/panels/I18nPanel.vue +738 -0
- package/src/modules/debug/components/panels/RouterPanel.vue +440 -0
- package/src/modules/debug/components/panels/ZonesPanel.vue +133 -0
- package/src/modules/debug/components/panels/index.ts +16 -0
- package/src/modules/debug/index.ts +58 -0
- package/src/modules/debug/styles.scss +2671 -0
- package/src/notifications/NotificationBadge.vue +35 -0
- package/src/notifications/NotificationListener.vue +71 -0
- package/src/notifications/NotificationModule.ts +76 -0
- package/src/notifications/NotificationPanel.vue +478 -0
- package/src/notifications/NotificationStore.ts +288 -0
- package/src/notifications/index.ts +42 -0
- package/src/notifications/styles.scss +32 -0
- package/src/orchestrator/Orchestrator.ts +427 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/useOrchestrator.ts +80 -0
- package/src/plugin.ts +251 -0
- package/src/query/FilterQuery.ts +306 -0
- package/src/query/QueryExecutor.ts +341 -0
- package/src/query/index.ts +16 -0
- package/src/security/EntityRolesProvider.ts +385 -0
- package/src/security/PermissionMatcher.ts +152 -0
- package/src/security/PermissionRegistry.ts +273 -0
- package/src/security/PersistableRolesProvider.ts +584 -0
- package/src/security/RolesManager.ts +136 -0
- package/src/security/RolesProvider.ts +167 -0
- package/src/security/RolesProviderStorage.ts +211 -0
- package/src/security/SecurityModule.ts +94 -0
- package/src/security/StaticRolesProvider.ts +117 -0
- package/src/security/UsersManager.ts +163 -0
- package/src/security/index.ts +77 -0
- package/src/security/pages/RoleForm.vue +276 -0
- package/src/security/pages/RoleList.vue +123 -0
- package/src/styles/_alerts.scss +95 -0
- package/src/styles/_buttons.scss +391 -0
- package/src/styles/_cards.scss +241 -0
- package/src/styles/_code.scss +78 -0
- package/src/styles/_dialogs.scss +55 -0
- package/src/styles/_filter-bar.scss +70 -0
- package/src/styles/_forms.scss +434 -0
- package/src/styles/_helpers.scss +153 -0
- package/src/styles/_lists.scss +225 -0
- package/src/styles/_main.scss +356 -0
- package/src/styles/_markdown.scss +88 -0
- package/src/styles/_responsive.scss +255 -0
- package/src/styles/_show-pages.scss +186 -0
- package/src/styles/_sidebar.scss +380 -0
- package/src/styles/_states.scss +115 -0
- package/src/styles/_stats.scss +47 -0
- package/src/styles/_variables.scss +224 -0
- package/src/styles/index.scss +38 -0
- package/src/styles/theme/_components.scss +331 -0
- package/src/styles/theme/_index.scss +10 -0
- package/src/styles/theme/_tokens.scss +147 -0
- package/src/styles/theme/_utilities.scss +175 -0
- package/src/toast/ToastBridgeModule.ts +68 -0
- package/src/toast/ToastListener.vue +55 -0
- package/src/toast/index.ts +26 -0
- package/src/toast/useSignalToast.ts +154 -0
- package/src/types/index.ts +285 -0
- package/src/types/pluralize.d.ts +19 -0
- package/src/types/quarkernel.d.ts +50 -0
- package/src/utils/formatters.ts +174 -0
- package/src/utils/index.ts +35 -0
- package/src/utils/transformers.ts +131 -0
- package/src/vite/qdadmDebugPlugin.ts +401 -0
- package/src/vite-env.d.ts +50 -0
- package/src/zones/ZoneRegistry.ts +934 -0
- package/src/zones/index.ts +29 -0
- 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,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
|
+
}
|