@consilioweb/payload-support 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -3
- package/dist/components/TicketConversation/hooks/useTranslation.cjs +25 -2
- package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
- package/dist/views/SupportDashboardView/client.cjs +1 -1
- package/dist/views/SupportDashboardView/client.js +1 -1
- package/dist/views/TicketingSettingsView/client.cjs +1 -1
- package/dist/views/TicketingSettingsView/client.js +1 -1
- package/dist/views/shared/locales/en.json +708 -0
- package/dist/views/shared/locales/fr.json +708 -0
- package/package.json +12 -12
- package/src/__tests__/translations.test.ts +67 -0
- package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
- package/src/views/SupportDashboardView/client.tsx +1 -1
- package/src/views/TicketingSettingsView/client.tsx +1 -1
- package/src/views/shared/locales/en.json +2 -2
- package/src/views/shared/locales/fr.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@consilioweb/payload-support",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Payload CMS plugin — professional support & ticketing system with AI, SLA, time tracking, live chat, and more",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -49,15 +49,6 @@
|
|
|
49
49
|
"README.md",
|
|
50
50
|
"LICENSE"
|
|
51
51
|
],
|
|
52
|
-
"scripts": {
|
|
53
|
-
"build": "tsup",
|
|
54
|
-
"typecheck": "tsc --noEmit",
|
|
55
|
-
"prepublishOnly": "npm run build",
|
|
56
|
-
"test": "vitest run",
|
|
57
|
-
"test:watch": "vitest",
|
|
58
|
-
"test:e2e": "playwright test",
|
|
59
|
-
"test:e2e:ui": "playwright test --ui"
|
|
60
|
-
},
|
|
61
52
|
"keywords": [
|
|
62
53
|
"payload",
|
|
63
54
|
"payload-cms",
|
|
@@ -125,12 +116,12 @@
|
|
|
125
116
|
},
|
|
126
117
|
"devDependencies": {
|
|
127
118
|
"@payloadcms/db-sqlite": "^3.86.0",
|
|
128
|
-
"@playwright/test": "^1.49.0",
|
|
129
119
|
"@payloadcms/graphql": "^3.86.0",
|
|
130
120
|
"@payloadcms/next": "^3.86.0",
|
|
131
121
|
"@payloadcms/richtext-lexical": "^3.86.0",
|
|
132
122
|
"@payloadcms/translations": "^3.86.0",
|
|
133
123
|
"@payloadcms/ui": "^3.86.0",
|
|
124
|
+
"@playwright/test": "^1.49.0",
|
|
134
125
|
"@types/pdfkit": "^0.17.6",
|
|
135
126
|
"@types/react": "^19.0.0",
|
|
136
127
|
"@types/sanitize-html": "^2.16.1",
|
|
@@ -140,6 +131,7 @@
|
|
|
140
131
|
"payload": "^3.86.0",
|
|
141
132
|
"react": "^19.2.7",
|
|
142
133
|
"react-dom": "^19.2.7",
|
|
134
|
+
"sharp": "0.34.5",
|
|
143
135
|
"tsup": "^8.0.0",
|
|
144
136
|
"typescript": "^5.5.0",
|
|
145
137
|
"vitest": "^4.1.9"
|
|
@@ -148,5 +140,13 @@
|
|
|
148
140
|
"pdfkit": "^0.19.1",
|
|
149
141
|
"sanitize-html": "^2.17.5",
|
|
150
142
|
"web-push": "^3.6.7"
|
|
143
|
+
},
|
|
144
|
+
"scripts": {
|
|
145
|
+
"build": "tsup",
|
|
146
|
+
"typecheck": "tsc --noEmit",
|
|
147
|
+
"test": "vitest run",
|
|
148
|
+
"test:watch": "vitest",
|
|
149
|
+
"test:e2e": "playwright test",
|
|
150
|
+
"test:e2e:ui": "playwright test --ui"
|
|
151
151
|
}
|
|
152
|
-
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import { getNestedValue, translations } from '../components/TicketConversation/hooks/useTranslation'
|
|
5
|
+
|
|
6
|
+
function collectSourceFiles(directory: string): string[] {
|
|
7
|
+
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
8
|
+
const path = join(directory, entry.name)
|
|
9
|
+
|
|
10
|
+
if (entry.isDirectory()) return collectSourceFiles(path)
|
|
11
|
+
return /\.(?:ts|tsx)$/.test(entry.name) ? [path] : []
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function collectTranslationKeys(): string[] {
|
|
16
|
+
const keys = new Set<string>()
|
|
17
|
+
const sourceDirectories = [join(process.cwd(), 'src/components'), join(process.cwd(), 'src/views')]
|
|
18
|
+
const translationCall = /\b(?:t|tFn)\(\s*(['"`])([^'"`]+)\1/g
|
|
19
|
+
|
|
20
|
+
for (const file of sourceDirectories.flatMap(collectSourceFiles)) {
|
|
21
|
+
const source = readFileSync(file, 'utf8')
|
|
22
|
+
|
|
23
|
+
for (const match of source.matchAll(translationCall)) {
|
|
24
|
+
keys.add(match[2])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [...keys].sort()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function flattenCatalog(value: Record<string, unknown>, prefix = ''): string[] {
|
|
32
|
+
return Object.entries(value).flatMap(([key, entry]) => {
|
|
33
|
+
const path = prefix ? `${prefix}.${key}` : key
|
|
34
|
+
|
|
35
|
+
if (entry != null && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
36
|
+
return flattenCatalog(entry as Record<string, unknown>, path)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return [path]
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('support translations', () => {
|
|
44
|
+
const usedKeys = collectTranslationKeys()
|
|
45
|
+
|
|
46
|
+
it.each(['fr', 'en'] as const)('defines every literal key used by support views in %s', (locale) => {
|
|
47
|
+
const catalogKeys = new Set(flattenCatalog(translations[locale]))
|
|
48
|
+
const missingKeys = usedKeys.filter((key) => !catalogKeys.has(key))
|
|
49
|
+
|
|
50
|
+
expect(missingKeys).toEqual([])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('keeps the French and English catalogs aligned', () => {
|
|
54
|
+
expect(flattenCatalog(translations.fr).sort()).toEqual(flattenCatalog(translations.en).sort())
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it.each([
|
|
58
|
+
'ticket.status.pending',
|
|
59
|
+
'inbox.timeAgo.now',
|
|
60
|
+
'detail.addTime',
|
|
61
|
+
'detail.addTagBtn',
|
|
62
|
+
'detail.billing.title',
|
|
63
|
+
])('resolves the regression key %s', (key) => {
|
|
64
|
+
expect(getNestedValue(translations.fr, key)).not.toBe(key)
|
|
65
|
+
expect(getNestedValue(translations.en, key)).not.toBe(key)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -1,22 +1,56 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect } from 'react'
|
|
2
2
|
import frTranslations from '../locales/fr.json'
|
|
3
3
|
import enTranslations from '../locales/en.json'
|
|
4
|
+
import frViewTranslations from '../../../views/shared/locales/fr.json'
|
|
5
|
+
import enViewTranslations from '../../../views/shared/locales/en.json'
|
|
4
6
|
|
|
5
7
|
type Locale = 'fr' | 'en'
|
|
6
8
|
|
|
7
9
|
const LOCALE_STORAGE_KEY = 'support_locale'
|
|
8
10
|
const DEFAULT_LOCALE: Locale = 'fr'
|
|
9
11
|
|
|
12
|
+
function mergeTranslations(
|
|
13
|
+
base: Record<string, unknown>,
|
|
14
|
+
supplement: Record<string, unknown>,
|
|
15
|
+
): Record<string, unknown> {
|
|
16
|
+
const merged = { ...base }
|
|
17
|
+
|
|
18
|
+
for (const [key, value] of Object.entries(supplement)) {
|
|
19
|
+
const current = merged[key]
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
current != null
|
|
23
|
+
&& value != null
|
|
24
|
+
&& typeof current === 'object'
|
|
25
|
+
&& typeof value === 'object'
|
|
26
|
+
&& !Array.isArray(current)
|
|
27
|
+
&& !Array.isArray(value)
|
|
28
|
+
) {
|
|
29
|
+
merged[key] = mergeTranslations(
|
|
30
|
+
current as Record<string, unknown>,
|
|
31
|
+
value as Record<string, unknown>,
|
|
32
|
+
)
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (current === undefined || (typeof current !== 'object' && typeof value === 'object')) {
|
|
37
|
+
merged[key] = value
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return merged
|
|
42
|
+
}
|
|
43
|
+
|
|
10
44
|
const translations: Record<Locale, Record<string, unknown>> = {
|
|
11
|
-
fr: frTranslations,
|
|
12
|
-
en: enTranslations,
|
|
45
|
+
fr: mergeTranslations(frTranslations, frViewTranslations),
|
|
46
|
+
en: mergeTranslations(enTranslations, enViewTranslations),
|
|
13
47
|
}
|
|
14
48
|
|
|
15
49
|
/**
|
|
16
50
|
* Resolve a dot-notation key from a nested object.
|
|
17
51
|
* e.g. getNestedValue(obj, 'ticket.status.open') => 'Ouvert'
|
|
18
52
|
*/
|
|
19
|
-
function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
53
|
+
export function getNestedValue(obj: Record<string, unknown>, path: string): string {
|
|
20
54
|
const keys = path.split('.')
|
|
21
55
|
let current: unknown = obj
|
|
22
56
|
|
|
@@ -113,4 +147,4 @@ export function useTranslation() {
|
|
|
113
147
|
}
|
|
114
148
|
|
|
115
149
|
export type { Locale }
|
|
116
|
-
export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY }
|
|
150
|
+
export { DEFAULT_LOCALE, LOCALE_STORAGE_KEY, translations }
|
|
@@ -540,7 +540,7 @@ export const SupportDashboardClient: React.FC = () => {
|
|
|
540
540
|
|
|
541
541
|
{/* CSAT ring */}
|
|
542
542
|
<div className={styles.panel}>
|
|
543
|
-
<h2 className={styles.panelTitle}>{t('dashboard.
|
|
543
|
+
<h2 className={styles.panelTitle}>{t('dashboard.csatTitle')}</h2>
|
|
544
544
|
<CSATRing score={stats.satisfactionAvg} count={stats.satisfactionCount} />
|
|
545
545
|
</div>
|
|
546
546
|
</div>
|
|
@@ -413,7 +413,7 @@ export const TicketingSettingsClient: React.FC = () => {
|
|
|
413
413
|
* SECTION 1 — Feature Flags
|
|
414
414
|
* ======================================== */}
|
|
415
415
|
<CollapsibleSection
|
|
416
|
-
title={t('settingsView.
|
|
416
|
+
title={t('settingsView.featuresTitle')}
|
|
417
417
|
icon={<Settings size={16} />}
|
|
418
418
|
color={V.blue}
|
|
419
419
|
badge={
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"activeTickets": "Active tickets",
|
|
267
267
|
"noActiveTickets": "No active tickets",
|
|
268
268
|
"volume7days": "Volume — last 7 days",
|
|
269
|
-
"
|
|
269
|
+
"csatTitle": "Client satisfaction (CSAT)",
|
|
270
270
|
"newTicketAction": "+ New ticket",
|
|
271
271
|
"pendingEmails": "Pending emails",
|
|
272
272
|
"crm": "CRM Clients",
|
|
@@ -599,7 +599,7 @@
|
|
|
599
599
|
"settingsView": {
|
|
600
600
|
"configTitle": "Support module configuration",
|
|
601
601
|
"intro": "Manage features, email configuration, AI and support module preferences.",
|
|
602
|
-
"
|
|
602
|
+
"featuresTitle": "Features",
|
|
603
603
|
"featuresDescription": "Enable or disable support module features.",
|
|
604
604
|
"reset": "Reset",
|
|
605
605
|
"saved": "✓ Saved",
|
|
@@ -266,7 +266,7 @@
|
|
|
266
266
|
"activeTickets": "Tickets actifs",
|
|
267
267
|
"noActiveTickets": "Aucun ticket actif",
|
|
268
268
|
"volume7days": "Volume — 7 derniers jours",
|
|
269
|
-
"
|
|
269
|
+
"csatTitle": "Satisfaction client (CSAT)",
|
|
270
270
|
"newTicketAction": "+ Nouveau ticket",
|
|
271
271
|
"pendingEmails": "Emails en attente",
|
|
272
272
|
"crm": "CRM Clients",
|
|
@@ -599,7 +599,7 @@
|
|
|
599
599
|
"settingsView": {
|
|
600
600
|
"configTitle": "Configuration du module Support",
|
|
601
601
|
"intro": "Gérez les fonctionnalités, la configuration email, l'IA et les préférences du module de support.",
|
|
602
|
-
"
|
|
602
|
+
"featuresTitle": "Fonctionnalités",
|
|
603
603
|
"featuresDescription": "Activez ou désactivez les fonctionnalités du module de support.",
|
|
604
604
|
"reset": "Réinitialiser",
|
|
605
605
|
"saved": "✓ Sauvegardé",
|