@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consilioweb/payload-support",
3
- "version": "2.0.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.csat')}</h2>
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.features')}
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
- "csat": "Client satisfaction (CSAT)",
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
- "features": "Features",
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
- "csat": "Satisfaction client (CSAT)",
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
- "features": "Fonctionnalités",
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é",