@wakastellar/ui 2.0.0 → 2.1.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.
Files changed (291) hide show
  1. package/README.md +71 -8
  2. package/dist/cli/commands/add.d.ts +7 -0
  3. package/dist/cli/commands/init.d.ts +6 -0
  4. package/dist/cli/commands/list.d.ts +5 -0
  5. package/dist/cli/commands/search.d.ts +1 -0
  6. package/dist/cli/index.cjs +6014 -0
  7. package/dist/cli/index.d.ts +1 -0
  8. package/dist/cli/utils/config.d.ts +29 -0
  9. package/dist/cli/utils/logger.d.ts +20 -0
  10. package/dist/cli/utils/registry.d.ts +23 -0
  11. package/package.json +14 -3
  12. package/src/blocks/activity-timeline/index.tsx +586 -0
  13. package/src/blocks/calendar-view/index.tsx +756 -0
  14. package/src/blocks/chat/index.tsx +1018 -0
  15. package/src/blocks/chat/widget.tsx +504 -0
  16. package/src/blocks/dashboard/index.tsx +522 -0
  17. package/src/blocks/empty-states/index.tsx +452 -0
  18. package/src/blocks/error-pages/index.tsx +426 -0
  19. package/src/blocks/faq/index.tsx +479 -0
  20. package/src/blocks/file-manager/index.tsx +890 -0
  21. package/src/blocks/footer/index.tsx +133 -0
  22. package/src/blocks/header/index.tsx +357 -0
  23. package/src/blocks/headtab/index.tsx +139 -0
  24. package/src/blocks/i18n-editor/index.tsx +1016 -0
  25. package/src/blocks/index.ts +80 -0
  26. package/src/blocks/kanban-board/index.tsx +779 -0
  27. package/src/blocks/landing/index.tsx +677 -0
  28. package/src/blocks/language-selector/index.tsx +88 -0
  29. package/src/blocks/layout/index.tsx +159 -0
  30. package/src/blocks/login/index.tsx +339 -0
  31. package/src/blocks/login/types.ts +131 -0
  32. package/src/blocks/pricing/index.tsx +564 -0
  33. package/src/blocks/profile/index.tsx +746 -0
  34. package/src/blocks/settings/index.tsx +558 -0
  35. package/src/blocks/sidebar/index.tsx +713 -0
  36. package/src/blocks/theme-creator-block/index.tsx +835 -0
  37. package/src/blocks/user-management/index.tsx +1037 -0
  38. package/src/blocks/wizard/index.tsx +719 -0
  39. package/src/components/DataTable/DataTable.tsx +406 -0
  40. package/src/components/DataTable/DataTableAdvanced.tsx +720 -0
  41. package/src/components/DataTable/DataTableBody.tsx +216 -0
  42. package/src/components/DataTable/DataTableCell.tsx +172 -0
  43. package/src/components/DataTable/DataTableColumnResizer.tsx +62 -0
  44. package/src/components/DataTable/DataTableConflictResolver.tsx +478 -0
  45. package/src/components/DataTable/DataTableContextMenu.tsx +219 -0
  46. package/src/components/DataTable/DataTableEditCell.tsx +279 -0
  47. package/src/components/DataTable/DataTableFilterBuilder.tsx +519 -0
  48. package/src/components/DataTable/DataTableFilters.tsx +535 -0
  49. package/src/components/DataTable/DataTableGrouping.tsx +147 -0
  50. package/src/components/DataTable/DataTableHeader.tsx +172 -0
  51. package/src/components/DataTable/DataTablePagination.tsx +125 -0
  52. package/src/components/DataTable/DataTableSelection.tsx +269 -0
  53. package/src/components/DataTable/DataTableSyncStatus.tsx +281 -0
  54. package/src/components/DataTable/DataTableToolbar.tsx +262 -0
  55. package/src/components/DataTable/README.md +446 -0
  56. package/src/components/DataTable/__tests__/DataTableAdvanced.test.tsx +426 -0
  57. package/src/components/DataTable/__tests__/DataTableEdit.test.tsx +329 -0
  58. package/src/components/DataTable/__tests__/useDataTableAdvanced.test.ts +455 -0
  59. package/src/components/DataTable/examples/EditExample.tsx +166 -0
  60. package/src/components/DataTable/formatters/index.ts +335 -0
  61. package/src/components/DataTable/hooks/__tests__/useDataTableEdit.test.ts +239 -0
  62. package/src/components/DataTable/hooks/useDataTable.ts +145 -0
  63. package/src/components/DataTable/hooks/useDataTableAdvanced.ts +342 -0
  64. package/src/components/DataTable/hooks/useDataTableAdvancedFilters.ts +637 -0
  65. package/src/components/DataTable/hooks/useDataTableColumnTemplates.ts +186 -0
  66. package/src/components/DataTable/hooks/useDataTableEdit.ts +167 -0
  67. package/src/components/DataTable/hooks/useDataTableExport.ts +227 -0
  68. package/src/components/DataTable/hooks/useDataTableImport.ts +216 -0
  69. package/src/components/DataTable/hooks/useDataTableOffline.ts +481 -0
  70. package/src/components/DataTable/hooks/useDataTableTheme.ts +213 -0
  71. package/src/components/DataTable/hooks/useDataTableVirtualization.ts +99 -0
  72. package/src/components/DataTable/hooks/useTableLayout.ts +85 -0
  73. package/src/components/DataTable/index.ts +81 -0
  74. package/src/components/DataTable/services/IndexedDBService.ts +504 -0
  75. package/src/components/DataTable/templates/index.tsx +803 -0
  76. package/src/components/DataTable/types.ts +504 -0
  77. package/src/components/DataTable/utils.ts +164 -0
  78. package/src/components/DataTable/workers/exportWorker.ts +213 -0
  79. package/src/components/accordion/index.tsx +61 -0
  80. package/src/components/alert/index.tsx +61 -0
  81. package/src/components/alert-dialog/index.tsx +146 -0
  82. package/src/components/aspect-ratio/index.tsx +12 -0
  83. package/src/components/avatar/index.tsx +54 -0
  84. package/src/components/badge/Badge.stories.tsx +64 -0
  85. package/src/components/badge/index.tsx +38 -0
  86. package/src/components/button/Button.stories.tsx +173 -0
  87. package/src/components/button/index.tsx +56 -0
  88. package/src/components/calendar/index.tsx +73 -0
  89. package/src/components/card/index.tsx +78 -0
  90. package/src/components/checkbox/index.tsx +34 -0
  91. package/src/components/code/index.tsx +229 -0
  92. package/src/components/collapsible/index.tsx +16 -0
  93. package/src/components/command/index.tsx +162 -0
  94. package/src/components/context-menu/index.tsx +204 -0
  95. package/src/components/dialog/index.tsx +126 -0
  96. package/src/components/dropdown-menu/index.tsx +204 -0
  97. package/src/components/error-boundary/ErrorBoundary.tsx +281 -0
  98. package/src/components/error-boundary/index.ts +7 -0
  99. package/src/components/form/index.tsx +183 -0
  100. package/src/components/hover-card/index.tsx +33 -0
  101. package/src/components/index.ts +368 -0
  102. package/src/components/input/Input.stories.tsx +100 -0
  103. package/src/components/input/index.tsx +27 -0
  104. package/src/components/input-otp/index.tsx +277 -0
  105. package/src/components/label/index.tsx +30 -0
  106. package/src/components/language-selector/index.tsx +341 -0
  107. package/src/components/menubar/index.tsx +240 -0
  108. package/src/components/navigation-menu/index.tsx +134 -0
  109. package/src/components/popover/index.tsx +35 -0
  110. package/src/components/progress/index.tsx +32 -0
  111. package/src/components/radio-group/index.tsx +48 -0
  112. package/src/components/scroll-area/index.tsx +52 -0
  113. package/src/components/select/index.tsx +164 -0
  114. package/src/components/separator/index.tsx +35 -0
  115. package/src/components/sheet/index.tsx +147 -0
  116. package/src/components/skeleton/index.tsx +22 -0
  117. package/src/components/slider/index.tsx +32 -0
  118. package/src/components/switch/index.tsx +33 -0
  119. package/src/components/table/index.tsx +117 -0
  120. package/src/components/tabs/index.tsx +59 -0
  121. package/src/components/textarea/index.tsx +30 -0
  122. package/src/components/theme-selector/index.tsx +327 -0
  123. package/src/components/toast/index.tsx +133 -0
  124. package/src/components/toaster/index.tsx +34 -0
  125. package/src/components/toggle/index.tsx +49 -0
  126. package/src/components/tooltip/index.tsx +34 -0
  127. package/src/components/typography/index.tsx +276 -0
  128. package/src/components/waka-3d-pie-chart/index.tsx +486 -0
  129. package/src/components/waka-achievement-unlock/index.tsx +716 -0
  130. package/src/components/waka-activity-feed/index.tsx +686 -0
  131. package/src/components/waka-address-autocomplete/index.tsx +1202 -0
  132. package/src/components/waka-admincrumb/index.tsx +349 -0
  133. package/src/components/waka-alert-stack/index.tsx +827 -0
  134. package/src/components/waka-allocation-matrix/index.tsx +1278 -0
  135. package/src/components/waka-approval-chain/index.tsx +766 -0
  136. package/src/components/waka-audit-log/index.tsx +1475 -0
  137. package/src/components/waka-autocomplete/index.tsx +358 -0
  138. package/src/components/waka-badge-showcase/index.tsx +704 -0
  139. package/src/components/waka-barcode/index.tsx +260 -0
  140. package/src/components/waka-biometric-prompt/index.tsx +765 -0
  141. package/src/components/waka-bottom-sheet/index.tsx +495 -0
  142. package/src/components/waka-breadcrumb/index.tsx +376 -0
  143. package/src/components/waka-breadcrumb-path/index.tsx +513 -0
  144. package/src/components/waka-budget-burn/index.tsx +1234 -0
  145. package/src/components/waka-capacity-planner/index.tsx +1107 -0
  146. package/src/components/waka-carousel/index.tsx +893 -0
  147. package/src/components/waka-cart-summary/index.tsx +1055 -0
  148. package/src/components/waka-challenge-timer/index.tsx +1044 -0
  149. package/src/components/waka-charts/WakaAreaChart.tsx +251 -0
  150. package/src/components/waka-charts/WakaBarChart.tsx +222 -0
  151. package/src/components/waka-charts/WakaChart.tsx +124 -0
  152. package/src/components/waka-charts/WakaLineChart.tsx +219 -0
  153. package/src/components/waka-charts/WakaMiniChart.tsx +133 -0
  154. package/src/components/waka-charts/WakaPieChart.tsx +214 -0
  155. package/src/components/waka-charts/WakaSparkline.tsx +229 -0
  156. package/src/components/waka-charts/dataTableHelpers.ts +109 -0
  157. package/src/components/waka-charts/hooks/useChartTheme.ts +123 -0
  158. package/src/components/waka-charts/hooks/useRechartsLoader.ts +234 -0
  159. package/src/components/waka-charts/index.ts +90 -0
  160. package/src/components/waka-charts/types.ts +330 -0
  161. package/src/components/waka-chat-bubble/index.tsx +1060 -0
  162. package/src/components/waka-checklist/index.tsx +1067 -0
  163. package/src/components/waka-checkout-stepper/index.tsx +976 -0
  164. package/src/components/waka-cohort-table/index.tsx +1011 -0
  165. package/src/components/waka-color-picker/index.tsx +447 -0
  166. package/src/components/waka-combo-counter/index.tsx +864 -0
  167. package/src/components/waka-combobox/index.tsx +497 -0
  168. package/src/components/waka-command-bar/index.tsx +403 -0
  169. package/src/components/waka-compare-period/index.tsx +1230 -0
  170. package/src/components/waka-connection-matrix/index.tsx +1053 -0
  171. package/src/components/waka-contribution-graph/index.tsx +552 -0
  172. package/src/components/waka-cost-breakdown/index.tsx +1065 -0
  173. package/src/components/waka-coupon-input/index.tsx +592 -0
  174. package/src/components/waka-credit-card-input/index.tsx +982 -0
  175. package/src/components/waka-daily-reward/index.tsx +762 -0
  176. package/src/components/waka-date-range-picker/index.tsx +378 -0
  177. package/src/components/waka-datetime-picker/index.tsx +793 -0
  178. package/src/components/waka-datetime-picker.form-integration/index.tsx +402 -0
  179. package/src/components/waka-deployment-lane/index.tsx +673 -0
  180. package/src/components/waka-device-trust/index.tsx +1259 -0
  181. package/src/components/waka-dock/index.tsx +285 -0
  182. package/src/components/waka-drawer/index.tsx +319 -0
  183. package/src/components/waka-empty-state/index.tsx +545 -0
  184. package/src/components/waka-error-shake/index.tsx +398 -0
  185. package/src/components/waka-feature-announcement/index.tsx +991 -0
  186. package/src/components/waka-file-upload/index.tsx +437 -0
  187. package/src/components/waka-floating-nav/index.tsx +413 -0
  188. package/src/components/waka-flow-diagram/index.tsx +508 -0
  189. package/src/components/waka-funnel-chart/index.tsx +823 -0
  190. package/src/components/waka-glow-card/index.tsx +246 -0
  191. package/src/components/waka-goal-progress/index.tsx +1025 -0
  192. package/src/components/waka-haptic-button/index.tsx +388 -0
  193. package/src/components/waka-health-pulse/index.tsx +451 -0
  194. package/src/components/waka-heatmap/index.tsx +1026 -0
  195. package/src/components/waka-hotspot/index.tsx +682 -0
  196. package/src/components/waka-image/index.tsx +373 -0
  197. package/src/components/waka-incident-timeline/index.tsx +686 -0
  198. package/src/components/waka-invoice-preview/index.tsx +829 -0
  199. package/src/components/waka-kanban/index.tsx +646 -0
  200. package/src/components/waka-kpi-dashboard/index.tsx +755 -0
  201. package/src/components/waka-leaderboard/index.tsx +746 -0
  202. package/src/components/waka-level-progress/index.tsx +665 -0
  203. package/src/components/waka-liquid-button/index.tsx +520 -0
  204. package/src/components/waka-loading-orbit/index.tsx +478 -0
  205. package/src/components/waka-loot-box/index.tsx +1091 -0
  206. package/src/components/waka-magic-link/index.tsx +321 -0
  207. package/src/components/waka-magnetic-button/index.tsx +567 -0
  208. package/src/components/waka-mention-input/index.tsx +953 -0
  209. package/src/components/waka-metric-sparkline/index.tsx +627 -0
  210. package/src/components/waka-milestone-road/index.tsx +1064 -0
  211. package/src/components/waka-modal/index.tsx +374 -0
  212. package/src/components/waka-morph-button/index.tsx +495 -0
  213. package/src/components/waka-network-topology/index.tsx +801 -0
  214. package/src/components/waka-notifications/index.tsx +414 -0
  215. package/src/components/waka-number-input/index.tsx +373 -0
  216. package/src/components/waka-orbital-menu/index.tsx +445 -0
  217. package/src/components/waka-order-tracker/index.tsx +1041 -0
  218. package/src/components/waka-pagination/index.tsx +393 -0
  219. package/src/components/waka-password-strength/index.tsx +824 -0
  220. package/src/components/waka-payment-method-picker/index.tsx +715 -0
  221. package/src/components/waka-permission-matrix/index.tsx +1302 -0
  222. package/src/components/waka-phone-input/index.tsx +801 -0
  223. package/src/components/waka-pipeline-view/index.tsx +604 -0
  224. package/src/components/waka-player-card/index.tsx +691 -0
  225. package/src/components/waka-points-popup/index.tsx +366 -0
  226. package/src/components/waka-power-up/index.tsx +1155 -0
  227. package/src/components/waka-presence-indicator/index.tsx +1181 -0
  228. package/src/components/waka-pricing-table/index.tsx +755 -0
  229. package/src/components/waka-product-card/index.tsx +786 -0
  230. package/src/components/waka-progress-onboarding/index.tsx +878 -0
  231. package/src/components/waka-pull-to-refresh/index.tsx +451 -0
  232. package/src/components/waka-qrcode/index.tsx +232 -0
  233. package/src/components/waka-quest-card/index.tsx +1275 -0
  234. package/src/components/waka-quota-bar/index.tsx +693 -0
  235. package/src/components/waka-radar-score/index.tsx +512 -0
  236. package/src/components/waka-rank-badge/index.tsx +813 -0
  237. package/src/components/waka-rating-input/index.tsx +560 -0
  238. package/src/components/waka-reaction-picker/index.tsx +1062 -0
  239. package/src/components/waka-region-map/index.tsx +730 -0
  240. package/src/components/waka-resource-gauge/index.tsx +654 -0
  241. package/src/components/waka-resource-pool/index.tsx +1035 -0
  242. package/src/components/waka-rich-text-editor/index.tsx +594 -0
  243. package/src/components/waka-rollback-slider/index.tsx +891 -0
  244. package/src/components/waka-sankey-diagram/index.tsx +1032 -0
  245. package/src/components/waka-schedule-picker/index.tsx +1060 -0
  246. package/src/components/waka-scratch-card/index.tsx +914 -0
  247. package/src/components/waka-season-pass/index.tsx +886 -0
  248. package/src/components/waka-security-score/index.tsx +1126 -0
  249. package/src/components/waka-segmented-control/index.tsx +238 -0
  250. package/src/components/waka-server-rack/index.tsx +764 -0
  251. package/src/components/waka-session-manager/index.tsx +815 -0
  252. package/src/components/waka-signature-pad/index.tsx +744 -0
  253. package/src/components/waka-skeleton-wave/index.tsx +454 -0
  254. package/src/components/waka-skill-tree/index.tsx +1031 -0
  255. package/src/components/waka-sla-tracker/index.tsx +798 -0
  256. package/src/components/waka-slider-range/index.tsx +765 -0
  257. package/src/components/waka-spin-wheel/index.tsx +671 -0
  258. package/src/components/waka-spinner/index.tsx +284 -0
  259. package/src/components/waka-spotlight/index.tsx +410 -0
  260. package/src/components/waka-stat/index.tsx +428 -0
  261. package/src/components/waka-stats-hexagon/index.tsx +824 -0
  262. package/src/components/waka-status-matrix/index.tsx +565 -0
  263. package/src/components/waka-stepper/index.tsx +489 -0
  264. package/src/components/waka-streak-counter/index.tsx +334 -0
  265. package/src/components/waka-success-explosion/index.tsx +453 -0
  266. package/src/components/waka-swipe-card/index.tsx +574 -0
  267. package/src/components/waka-tabs-morph/index.tsx +509 -0
  268. package/src/components/waka-tag-input/index.tsx +877 -0
  269. package/src/components/waka-team-banner/index.tsx +1183 -0
  270. package/src/components/waka-terminal-output/index.tsx +836 -0
  271. package/src/components/waka-theme-creator/index.tsx +762 -0
  272. package/src/components/waka-theme-manager/index.tsx +654 -0
  273. package/src/components/waka-thread-view/index.tsx +874 -0
  274. package/src/components/waka-tilt-card/index.tsx +250 -0
  275. package/src/components/waka-time-picker/index.tsx +479 -0
  276. package/src/components/waka-timeline/index.tsx +385 -0
  277. package/src/components/waka-tooltip-tour/index.tsx +855 -0
  278. package/src/components/waka-tour-guide/index.tsx +920 -0
  279. package/src/components/waka-tournament-bracket/index.tsx +1276 -0
  280. package/src/components/waka-tree/index.tsx +557 -0
  281. package/src/components/waka-treemap-chart/index.tsx +1031 -0
  282. package/src/components/waka-two-factor-setup/index.tsx +995 -0
  283. package/src/components/waka-typewriter/index.tsx +566 -0
  284. package/src/components/waka-typing-indicator/index.tsx +649 -0
  285. package/src/components/waka-versus-card/index.tsx +1026 -0
  286. package/src/components/waka-video/index.tsx +557 -0
  287. package/src/components/waka-video-call/index.tsx +1087 -0
  288. package/src/components/waka-virtual-list/index.tsx +327 -0
  289. package/src/components/waka-voice-message/index.tsx +1019 -0
  290. package/src/components/waka-welcome-modal/index.tsx +790 -0
  291. package/src/components/waka-xp-bar/index.tsx +799 -0
@@ -0,0 +1,1016 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../../utils/cn"
5
+ import { Button } from "../../components/button"
6
+ import { Input } from "../../components/input"
7
+ import { Badge } from "../../components/badge"
8
+ import { Textarea } from "../../components/textarea"
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "../../components/select"
16
+ import {
17
+ Sheet,
18
+ SheetContent,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ SheetDescription,
22
+ SheetFooter,
23
+ } from "../../components/sheet"
24
+ import {
25
+ Dialog,
26
+ DialogContent,
27
+ DialogHeader,
28
+ DialogTitle,
29
+ DialogDescription,
30
+ DialogFooter,
31
+ } from "../../components/dialog"
32
+ import {
33
+ DropdownMenu,
34
+ DropdownMenuContent,
35
+ DropdownMenuItem,
36
+ DropdownMenuSeparator,
37
+ DropdownMenuTrigger,
38
+ } from "../../components/dropdown-menu"
39
+ import {
40
+ Tooltip,
41
+ TooltipContent,
42
+ TooltipProvider,
43
+ TooltipTrigger,
44
+ } from "../../components/tooltip"
45
+ import {
46
+ AlertTriangle,
47
+ Check,
48
+ CheckCircle,
49
+ ChevronDown,
50
+ ChevronRight,
51
+ Copy,
52
+ Download,
53
+ Edit3,
54
+ Filter,
55
+ Flag,
56
+ Languages,
57
+ Loader2,
58
+ MoreHorizontal,
59
+ Plus,
60
+ Search,
61
+ Trash2,
62
+ Upload,
63
+ X,
64
+ XCircle,
65
+ } from "lucide-react"
66
+
67
+ // Types
68
+ export interface TranslationValue {
69
+ value: string
70
+ updatedAt?: Date
71
+ comment?: string
72
+ }
73
+
74
+ export interface TranslationEntry {
75
+ key: string
76
+ translations: Record<string, TranslationValue>
77
+ }
78
+
79
+ export interface LanguageConfig {
80
+ code: string
81
+ label: string
82
+ flag?: string
83
+ isSource?: boolean
84
+ }
85
+
86
+ export interface I18nEditorConfig {
87
+ /** Langues disponibles */
88
+ languages: LanguageConfig[]
89
+ /** Code de la langue source */
90
+ sourceLanguage: string
91
+ /** Séparateur pour les clés (ex: ".") */
92
+ keyPathSeparator?: string
93
+ /** Activer l'auto-save */
94
+ autoSave?: boolean
95
+ /** Délai de debounce pour l'auto-save en ms */
96
+ saveDebounceMs?: number
97
+ /** Patterns de placeholders à valider */
98
+ placeholderPatterns?: string[]
99
+ /** Ratio max de longueur */
100
+ maxLengthRatio?: number
101
+ }
102
+
103
+ export interface I18nEditorProps {
104
+ /** Configuration de l'éditeur */
105
+ config: I18nEditorConfig
106
+ /** Données de traduction initiales */
107
+ translations: TranslationEntry[]
108
+ /** Callback quand une traduction change */
109
+ onChange?: (key: string, language: string, value: string) => void
110
+ /** Callback pour sauvegarder */
111
+ onSave?: (translations: TranslationEntry[]) => Promise<void>
112
+ /** Callback pour ajouter une clé */
113
+ onAddKey?: (key: string) => Promise<void>
114
+ /** Callback pour supprimer une clé */
115
+ onDeleteKey?: (key: string) => Promise<void>
116
+ /** Callback pour ajouter une langue */
117
+ onAddLanguage?: (language: LanguageConfig) => Promise<void>
118
+ /** Callback pour exporter */
119
+ onExport?: (language?: string) => Promise<void>
120
+ /** Callback pour importer */
121
+ onImport?: (file: File, language: string, strategy: "overwrite" | "merge") => Promise<void>
122
+ /** État de chargement */
123
+ isLoading?: boolean
124
+ /** État de sauvegarde */
125
+ isSaving?: boolean
126
+ /** Classe CSS */
127
+ className?: string
128
+ /** Titre */
129
+ title?: string
130
+ /** Description */
131
+ description?: string
132
+ }
133
+
134
+ type TranslationStatus = "valid" | "missing" | "identical" | "warning"
135
+ type FilterType = "all" | "missing" | "incomplete" | "identical"
136
+
137
+ // Helper functions
138
+ function getTranslationStatus(
139
+ entry: TranslationEntry,
140
+ targetLang: string,
141
+ sourceLang: string
142
+ ): TranslationStatus {
143
+ const sourceValue = entry.translations[sourceLang]?.value || ""
144
+ const targetValue = entry.translations[targetLang]?.value
145
+
146
+ if (!targetValue || targetValue.trim() === "") {
147
+ return "missing"
148
+ }
149
+ if (targetValue === sourceValue) {
150
+ return "identical"
151
+ }
152
+ return "valid"
153
+ }
154
+
155
+ function getOverallStatus(
156
+ entry: TranslationEntry,
157
+ languages: string[],
158
+ sourceLang: string
159
+ ): TranslationStatus {
160
+ const statuses = languages
161
+ .filter(lang => lang !== sourceLang)
162
+ .map(lang => getTranslationStatus(entry, lang, sourceLang))
163
+
164
+ if (statuses.includes("missing")) return "missing"
165
+ if (statuses.includes("identical")) return "identical"
166
+ return "valid"
167
+ }
168
+
169
+ function validatePlaceholders(
170
+ source: string,
171
+ target: string,
172
+ patterns: string[]
173
+ ): { valid: boolean; missing: string[]; extra: string[] } {
174
+ const extractMatches = (text: string) => {
175
+ const matches: string[] = []
176
+ for (const pattern of patterns) {
177
+ const regex = new RegExp(pattern, "g")
178
+ let match
179
+ while ((match = regex.exec(text)) !== null) {
180
+ matches.push(match[0])
181
+ }
182
+ }
183
+ return matches
184
+ }
185
+
186
+ const sourceMatches = extractMatches(source)
187
+ const targetMatches = extractMatches(target)
188
+
189
+ const missing = sourceMatches.filter(m => !targetMatches.includes(m))
190
+ const extra = targetMatches.filter(m => !sourceMatches.includes(m))
191
+
192
+ return {
193
+ valid: missing.length === 0 && extra.length === 0,
194
+ missing,
195
+ extra,
196
+ }
197
+ }
198
+
199
+ function groupByPrefix(
200
+ entries: TranslationEntry[],
201
+ separator: string
202
+ ): Map<string, TranslationEntry[]> {
203
+ const groups = new Map<string, TranslationEntry[]>()
204
+
205
+ for (const entry of entries) {
206
+ const parts = entry.key.split(separator)
207
+ const prefix = parts.length > 1 ? parts[0] : "__root__"
208
+
209
+ if (!groups.has(prefix)) {
210
+ groups.set(prefix, [])
211
+ }
212
+ groups.get(prefix)!.push(entry)
213
+ }
214
+
215
+ return groups
216
+ }
217
+
218
+ // Status Badge Component
219
+ function StatusBadge({ status }: { status: TranslationStatus }) {
220
+ const config = {
221
+ valid: { label: "OK", variant: "default" as const, icon: CheckCircle, className: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" },
222
+ missing: { label: "Manquant", variant: "destructive" as const, icon: XCircle, className: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" },
223
+ identical: { label: "Identique", variant: "secondary" as const, icon: AlertTriangle, className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" },
224
+ warning: { label: "Attention", variant: "secondary" as const, icon: AlertTriangle, className: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" },
225
+ }
226
+
227
+ const { label, icon: Icon, className } = config[status]
228
+
229
+ return (
230
+ <Badge variant="outline" className={cn("gap-1 text-xs", className)}>
231
+ <Icon className="h-3 w-3" />
232
+ {label}
233
+ </Badge>
234
+ )
235
+ }
236
+
237
+ // Inline Editor Component
238
+ function InlineEditor({
239
+ value,
240
+ onChange,
241
+ onBlur,
242
+ placeholder,
243
+ status,
244
+ disabled,
245
+ }: {
246
+ value: string
247
+ onChange: (value: string) => void
248
+ onBlur?: () => void
249
+ placeholder?: string
250
+ status?: TranslationStatus
251
+ disabled?: boolean
252
+ }) {
253
+ const [localValue, setLocalValue] = React.useState(value)
254
+
255
+ React.useEffect(() => {
256
+ setLocalValue(value)
257
+ }, [value])
258
+
259
+ const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
260
+ setLocalValue(e.target.value)
261
+ onChange(e.target.value)
262
+ }
263
+
264
+ return (
265
+ <Textarea
266
+ value={localValue}
267
+ onChange={handleChange}
268
+ onBlur={onBlur}
269
+ placeholder={placeholder}
270
+ disabled={disabled}
271
+ className={cn(
272
+ "min-h-[60px] resize-none text-sm",
273
+ status === "missing" && "border-red-300 dark:border-red-700",
274
+ status === "identical" && "border-yellow-300 dark:border-yellow-700"
275
+ )}
276
+ />
277
+ )
278
+ }
279
+
280
+ // Main Component
281
+ export function WakaI18nEditor({
282
+ config,
283
+ translations: initialTranslations,
284
+ onChange,
285
+ onSave,
286
+ onAddKey,
287
+ onDeleteKey,
288
+ onAddLanguage,
289
+ onExport,
290
+ onImport,
291
+ isLoading = false,
292
+ isSaving = false,
293
+ className,
294
+ title = "Translation Editor",
295
+ description = "Gérez vos traductions i18n",
296
+ }: I18nEditorProps) {
297
+ const [translations, setTranslations] = React.useState<TranslationEntry[]>(initialTranslations)
298
+ const [searchQuery, setSearchQuery] = React.useState("")
299
+ const [activeFilter, setActiveFilter] = React.useState<FilterType>("all")
300
+ const [collapsedGroups, setCollapsedGroups] = React.useState<Set<string>>(new Set())
301
+ const [selectedEntry, setSelectedEntry] = React.useState<TranslationEntry | null>(null)
302
+ const [isEditorOpen, setIsEditorOpen] = React.useState(false)
303
+ const [isAddKeyOpen, setIsAddKeyOpen] = React.useState(false)
304
+ const [isAddLanguageOpen, setIsAddLanguageOpen] = React.useState(false)
305
+ const [isImportOpen, setIsImportOpen] = React.useState(false)
306
+ const [newKey, setNewKey] = React.useState("")
307
+ const [newLanguage, setNewLanguage] = React.useState({ code: "", label: "", flag: "" })
308
+ const [importFile, setImportFile] = React.useState<File | null>(null)
309
+ const [importLanguage, setImportLanguage] = React.useState("")
310
+ const [importStrategy, setImportStrategy] = React.useState<"overwrite" | "merge">("merge")
311
+ const [pendingChanges, setPendingChanges] = React.useState<Map<string, Map<string, string>>>(new Map())
312
+ const saveTimeoutRef = React.useRef<NodeJS.Timeout | null>(null)
313
+
314
+ const {
315
+ languages,
316
+ sourceLanguage,
317
+ keyPathSeparator = ".",
318
+ autoSave = true,
319
+ saveDebounceMs = 300,
320
+ placeholderPatterns = ["{{.*?}}", "{.*?}", "%s", "%d"],
321
+ } = config
322
+
323
+ const targetLanguages = languages.filter(l => l.code !== sourceLanguage)
324
+
325
+ // Update translations when props change
326
+ React.useEffect(() => {
327
+ setTranslations(initialTranslations)
328
+ }, [initialTranslations])
329
+
330
+ // Auto-save logic
331
+ React.useEffect(() => {
332
+ if (autoSave && pendingChanges.size > 0 && onSave) {
333
+ if (saveTimeoutRef.current) {
334
+ clearTimeout(saveTimeoutRef.current)
335
+ }
336
+
337
+ saveTimeoutRef.current = setTimeout(async () => {
338
+ await onSave(translations)
339
+ setPendingChanges(new Map())
340
+ }, saveDebounceMs)
341
+ }
342
+
343
+ return () => {
344
+ if (saveTimeoutRef.current) {
345
+ clearTimeout(saveTimeoutRef.current)
346
+ }
347
+ }
348
+ }, [pendingChanges, autoSave, saveDebounceMs, onSave, translations])
349
+
350
+ // Filter and search logic
351
+ const filteredTranslations = React.useMemo(() => {
352
+ let result = translations
353
+
354
+ // Search filter
355
+ if (searchQuery) {
356
+ const query = searchQuery.toLowerCase()
357
+ result = result.filter(entry => {
358
+ if (entry.key.toLowerCase().includes(query)) return true
359
+ for (const lang of Object.keys(entry.translations)) {
360
+ if (entry.translations[lang]?.value?.toLowerCase().includes(query)) return true
361
+ }
362
+ return false
363
+ })
364
+ }
365
+
366
+ // Status filter
367
+ if (activeFilter !== "all") {
368
+ result = result.filter(entry => {
369
+ const status = getOverallStatus(entry, languages.map(l => l.code), sourceLanguage)
370
+ switch (activeFilter) {
371
+ case "missing":
372
+ return status === "missing"
373
+ case "incomplete":
374
+ return status === "missing" || status === "identical"
375
+ case "identical":
376
+ return status === "identical"
377
+ default:
378
+ return true
379
+ }
380
+ })
381
+ }
382
+
383
+ return result
384
+ }, [translations, searchQuery, activeFilter, languages, sourceLanguage])
385
+
386
+ // Group translations
387
+ const groupedTranslations = React.useMemo(() => {
388
+ return groupByPrefix(filteredTranslations, keyPathSeparator)
389
+ }, [filteredTranslations, keyPathSeparator])
390
+
391
+ // Handlers
392
+ const handleTranslationChange = (key: string, language: string, value: string) => {
393
+ setTranslations(prev =>
394
+ prev.map(entry => {
395
+ if (entry.key === key) {
396
+ return {
397
+ ...entry,
398
+ translations: {
399
+ ...entry.translations,
400
+ [language]: {
401
+ ...entry.translations[language],
402
+ value,
403
+ updatedAt: new Date(),
404
+ },
405
+ },
406
+ }
407
+ }
408
+ return entry
409
+ })
410
+ )
411
+
412
+ // Track pending changes
413
+ setPendingChanges(prev => {
414
+ const newChanges = new Map(prev)
415
+ if (!newChanges.has(key)) {
416
+ newChanges.set(key, new Map())
417
+ }
418
+ newChanges.get(key)!.set(language, value)
419
+ return newChanges
420
+ })
421
+
422
+ onChange?.(key, language, value)
423
+ }
424
+
425
+ const handleAddKey = async () => {
426
+ if (!newKey.trim()) return
427
+
428
+ await onAddKey?.(newKey)
429
+ setNewKey("")
430
+ setIsAddKeyOpen(false)
431
+ }
432
+
433
+ const handleDeleteKey = async (key: string) => {
434
+ await onDeleteKey?.(key)
435
+ setTranslations(prev => prev.filter(e => e.key !== key))
436
+ }
437
+
438
+ const handleAddLanguage = async () => {
439
+ if (!newLanguage.code || !newLanguage.label) return
440
+
441
+ await onAddLanguage?.(newLanguage)
442
+ setNewLanguage({ code: "", label: "", flag: "" })
443
+ setIsAddLanguageOpen(false)
444
+ }
445
+
446
+ const handleImport = async () => {
447
+ if (!importFile || !importLanguage) return
448
+
449
+ await onImport?.(importFile, importLanguage, importStrategy)
450
+ setImportFile(null)
451
+ setImportLanguage("")
452
+ setIsImportOpen(false)
453
+ }
454
+
455
+ const handleCopyKey = (key: string) => {
456
+ navigator.clipboard.writeText(key)
457
+ }
458
+
459
+ const toggleGroup = (group: string) => {
460
+ setCollapsedGroups(prev => {
461
+ const newSet = new Set(prev)
462
+ if (newSet.has(group)) {
463
+ newSet.delete(group)
464
+ } else {
465
+ newSet.add(group)
466
+ }
467
+ return newSet
468
+ })
469
+ }
470
+
471
+ // Stats
472
+ const stats = React.useMemo(() => {
473
+ let total = translations.length
474
+ let missing = 0
475
+ let identical = 0
476
+ let valid = 0
477
+
478
+ for (const entry of translations) {
479
+ const status = getOverallStatus(entry, languages.map(l => l.code), sourceLanguage)
480
+ if (status === "missing") missing++
481
+ else if (status === "identical") identical++
482
+ else valid++
483
+ }
484
+
485
+ return { total, missing, identical, valid }
486
+ }, [translations, languages, sourceLanguage])
487
+
488
+ if (isLoading) {
489
+ return (
490
+ <div className={cn("flex items-center justify-center py-12", className)}>
491
+ <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
492
+ </div>
493
+ )
494
+ }
495
+
496
+ return (
497
+ <TooltipProvider>
498
+ <div className={cn("flex flex-col h-full", className)}>
499
+ {/* Header */}
500
+ <div className="flex flex-col gap-4 p-4 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
501
+ <div className="flex items-center justify-between">
502
+ <div>
503
+ <h2 className="text-2xl font-bold flex items-center gap-2">
504
+ <Languages className="h-6 w-6" />
505
+ {title}
506
+ </h2>
507
+ <p className="text-sm text-muted-foreground">{description}</p>
508
+ </div>
509
+
510
+ {/* Actions */}
511
+ <div className="flex items-center gap-2">
512
+ <Button
513
+ variant="outline"
514
+ size="sm"
515
+ onClick={() => setIsAddKeyOpen(true)}
516
+ >
517
+ <Plus className="h-4 w-4 mr-1" />
518
+ Ajouter une clé
519
+ </Button>
520
+ <Button
521
+ variant="outline"
522
+ size="sm"
523
+ onClick={() => setIsAddLanguageOpen(true)}
524
+ >
525
+ <Flag className="h-4 w-4 mr-1" />
526
+ Ajouter une langue
527
+ </Button>
528
+ <DropdownMenu>
529
+ <DropdownMenuTrigger asChild>
530
+ <Button variant="outline" size="sm">
531
+ <MoreHorizontal className="h-4 w-4" />
532
+ </Button>
533
+ </DropdownMenuTrigger>
534
+ <DropdownMenuContent align="end">
535
+ <DropdownMenuItem onClick={() => onExport?.()}>
536
+ <Download className="h-4 w-4 mr-2" />
537
+ Exporter tout
538
+ </DropdownMenuItem>
539
+ {languages.map(lang => (
540
+ <DropdownMenuItem key={lang.code} onClick={() => onExport?.(lang.code)}>
541
+ <Download className="h-4 w-4 mr-2" />
542
+ Exporter {lang.label}
543
+ </DropdownMenuItem>
544
+ ))}
545
+ <DropdownMenuSeparator />
546
+ <DropdownMenuItem onClick={() => setIsImportOpen(true)}>
547
+ <Upload className="h-4 w-4 mr-2" />
548
+ Importer
549
+ </DropdownMenuItem>
550
+ </DropdownMenuContent>
551
+ </DropdownMenu>
552
+
553
+ {isSaving && (
554
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
555
+ <Loader2 className="h-4 w-4 animate-spin" />
556
+ Sauvegarde...
557
+ </div>
558
+ )}
559
+ </div>
560
+ </div>
561
+
562
+ {/* Stats */}
563
+ <div className="flex items-center gap-4 text-sm">
564
+ <span className="text-muted-foreground">
565
+ {stats.total} clés
566
+ </span>
567
+ <span className="flex items-center gap-1 text-green-600 dark:text-green-400">
568
+ <CheckCircle className="h-4 w-4" />
569
+ {stats.valid} traduites
570
+ </span>
571
+ <span className="flex items-center gap-1 text-red-600 dark:text-red-400">
572
+ <XCircle className="h-4 w-4" />
573
+ {stats.missing} manquantes
574
+ </span>
575
+ <span className="flex items-center gap-1 text-yellow-600 dark:text-yellow-400">
576
+ <AlertTriangle className="h-4 w-4" />
577
+ {stats.identical} identiques
578
+ </span>
579
+ </div>
580
+
581
+ {/* Search and Filters */}
582
+ <div className="flex items-center gap-4">
583
+ <div className="relative flex-1 max-w-md">
584
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
585
+ <Input
586
+ placeholder="Rechercher une clé ou une traduction..."
587
+ value={searchQuery}
588
+ onChange={e => setSearchQuery(e.target.value)}
589
+ className="pl-10"
590
+ />
591
+ {searchQuery && (
592
+ <button
593
+ onClick={() => setSearchQuery("")}
594
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
595
+ >
596
+ <X className="h-4 w-4" />
597
+ </button>
598
+ )}
599
+ </div>
600
+
601
+ <div className="flex items-center gap-2">
602
+ <Filter className="h-4 w-4 text-muted-foreground" />
603
+ <Select value={activeFilter} onValueChange={(v) => setActiveFilter(v as FilterType)}>
604
+ <SelectTrigger className="w-[180px]">
605
+ <SelectValue />
606
+ </SelectTrigger>
607
+ <SelectContent>
608
+ <SelectItem value="all">Toutes ({stats.total})</SelectItem>
609
+ <SelectItem value="missing">Manquantes ({stats.missing})</SelectItem>
610
+ <SelectItem value="incomplete">Incomplètes ({stats.missing + stats.identical})</SelectItem>
611
+ <SelectItem value="identical">Identiques ({stats.identical})</SelectItem>
612
+ </SelectContent>
613
+ </Select>
614
+ </div>
615
+ </div>
616
+ </div>
617
+
618
+ {/* Table */}
619
+ <div className="flex-1 overflow-auto">
620
+ <table className="w-full border-collapse">
621
+ <thead className="sticky top-0 bg-muted/50 backdrop-blur z-10">
622
+ <tr>
623
+ <th className="text-left p-3 font-medium text-sm border-b w-[280px]">
624
+ Clé
625
+ </th>
626
+ <th className="text-left p-3 font-medium text-sm border-b">
627
+ {languages.find(l => l.code === sourceLanguage)?.label || sourceLanguage} (source)
628
+ </th>
629
+ {targetLanguages.map(lang => (
630
+ <th key={lang.code} className="text-left p-3 font-medium text-sm border-b">
631
+ <span className="flex items-center gap-2">
632
+ {lang.flag && <span>{lang.flag}</span>}
633
+ {lang.label}
634
+ </span>
635
+ </th>
636
+ ))}
637
+ <th className="text-center p-3 font-medium text-sm border-b w-[100px]">
638
+ État
639
+ </th>
640
+ <th className="text-center p-3 font-medium text-sm border-b w-[60px]">
641
+ Actions
642
+ </th>
643
+ </tr>
644
+ </thead>
645
+ <tbody>
646
+ {Array.from(groupedTranslations.entries()).map(([group, entries]) => (
647
+ <React.Fragment key={group}>
648
+ {/* Group Header */}
649
+ {group !== "__root__" && (
650
+ <tr className="bg-muted/30">
651
+ <td colSpan={targetLanguages.length + 4} className="p-2">
652
+ <button
653
+ onClick={() => toggleGroup(group)}
654
+ className="flex items-center gap-2 text-sm font-medium hover:text-primary transition-colors"
655
+ >
656
+ {collapsedGroups.has(group) ? (
657
+ <ChevronRight className="h-4 w-4" />
658
+ ) : (
659
+ <ChevronDown className="h-4 w-4" />
660
+ )}
661
+ {group}
662
+ <Badge variant="secondary" className="text-xs">
663
+ {entries.length}
664
+ </Badge>
665
+ </button>
666
+ </td>
667
+ </tr>
668
+ )}
669
+
670
+ {/* Group Entries */}
671
+ {!collapsedGroups.has(group) &&
672
+ entries.map(entry => {
673
+ const overallStatus = getOverallStatus(
674
+ entry,
675
+ languages.map(l => l.code),
676
+ sourceLanguage
677
+ )
678
+
679
+ return (
680
+ <tr
681
+ key={entry.key}
682
+ className="border-b hover:bg-muted/30 transition-colors"
683
+ >
684
+ {/* Key */}
685
+ <td className="p-3 font-mono text-sm">
686
+ <div className="flex items-center gap-2">
687
+ <span className="truncate max-w-[200px]" title={entry.key}>
688
+ {entry.key}
689
+ </span>
690
+ <Tooltip>
691
+ <TooltipTrigger asChild>
692
+ <button
693
+ onClick={() => handleCopyKey(entry.key)}
694
+ className="text-muted-foreground hover:text-foreground"
695
+ >
696
+ <Copy className="h-3 w-3" />
697
+ </button>
698
+ </TooltipTrigger>
699
+ <TooltipContent>Copier la clé</TooltipContent>
700
+ </Tooltip>
701
+ </div>
702
+ </td>
703
+
704
+ {/* Source */}
705
+ <td className="p-2">
706
+ <InlineEditor
707
+ value={entry.translations[sourceLanguage]?.value || ""}
708
+ onChange={value => handleTranslationChange(entry.key, sourceLanguage, value)}
709
+ placeholder={`Texte source...`}
710
+ status={entry.translations[sourceLanguage]?.value ? "valid" : "missing"}
711
+ />
712
+ </td>
713
+
714
+ {/* Target Languages */}
715
+ {targetLanguages.map(lang => {
716
+ const status = getTranslationStatus(entry, lang.code, sourceLanguage)
717
+ return (
718
+ <td key={lang.code} className="p-2">
719
+ <InlineEditor
720
+ value={entry.translations[lang.code]?.value || ""}
721
+ onChange={value => handleTranslationChange(entry.key, lang.code, value)}
722
+ placeholder={`Traduction ${lang.label}...`}
723
+ status={status}
724
+ />
725
+ </td>
726
+ )
727
+ })}
728
+
729
+ {/* Status */}
730
+ <td className="p-3 text-center">
731
+ <StatusBadge status={overallStatus} />
732
+ </td>
733
+
734
+ {/* Actions */}
735
+ <td className="p-3 text-center">
736
+ <DropdownMenu>
737
+ <DropdownMenuTrigger asChild>
738
+ <Button variant="ghost" size="sm">
739
+ <MoreHorizontal className="h-4 w-4" />
740
+ </Button>
741
+ </DropdownMenuTrigger>
742
+ <DropdownMenuContent align="end">
743
+ <DropdownMenuItem
744
+ onClick={() => {
745
+ setSelectedEntry(entry)
746
+ setIsEditorOpen(true)
747
+ }}
748
+ >
749
+ <Edit3 className="h-4 w-4 mr-2" />
750
+ Éditer
751
+ </DropdownMenuItem>
752
+ <DropdownMenuItem onClick={() => handleCopyKey(entry.key)}>
753
+ <Copy className="h-4 w-4 mr-2" />
754
+ Copier la clé
755
+ </DropdownMenuItem>
756
+ <DropdownMenuSeparator />
757
+ <DropdownMenuItem
758
+ className="text-destructive"
759
+ onClick={() => handleDeleteKey(entry.key)}
760
+ >
761
+ <Trash2 className="h-4 w-4 mr-2" />
762
+ Supprimer
763
+ </DropdownMenuItem>
764
+ </DropdownMenuContent>
765
+ </DropdownMenu>
766
+ </td>
767
+ </tr>
768
+ )
769
+ })}
770
+ </React.Fragment>
771
+ ))}
772
+
773
+ {filteredTranslations.length === 0 && (
774
+ <tr>
775
+ <td colSpan={targetLanguages.length + 4} className="p-12 text-center">
776
+ <Languages className="h-12 w-12 mx-auto text-muted-foreground/50 mb-4" />
777
+ <p className="text-muted-foreground">
778
+ {searchQuery || activeFilter !== "all"
779
+ ? "Aucune traduction ne correspond à vos critères"
780
+ : "Aucune traduction disponible"}
781
+ </p>
782
+ </td>
783
+ </tr>
784
+ )}
785
+ </tbody>
786
+ </table>
787
+ </div>
788
+
789
+ {/* Editor Drawer */}
790
+ <Sheet open={isEditorOpen} onOpenChange={setIsEditorOpen}>
791
+ <SheetContent className="w-[500px] sm:max-w-[500px]">
792
+ <SheetHeader>
793
+ <SheetTitle>Éditer la traduction</SheetTitle>
794
+ <SheetDescription>
795
+ {selectedEntry?.key}
796
+ </SheetDescription>
797
+ </SheetHeader>
798
+
799
+ {selectedEntry && (
800
+ <div className="py-6 space-y-6">
801
+ {/* Source */}
802
+ <div className="space-y-2">
803
+ <label className="text-sm font-medium">
804
+ {languages.find(l => l.code === sourceLanguage)?.label} (source)
805
+ </label>
806
+ <Textarea
807
+ value={selectedEntry.translations[sourceLanguage]?.value || ""}
808
+ onChange={e => {
809
+ handleTranslationChange(selectedEntry.key, sourceLanguage, e.target.value)
810
+ setSelectedEntry({
811
+ ...selectedEntry,
812
+ translations: {
813
+ ...selectedEntry.translations,
814
+ [sourceLanguage]: {
815
+ ...selectedEntry.translations[sourceLanguage],
816
+ value: e.target.value,
817
+ },
818
+ },
819
+ })
820
+ }}
821
+ placeholder="Texte source..."
822
+ className="min-h-[100px]"
823
+ />
824
+ </div>
825
+
826
+ {/* Targets */}
827
+ {languages.filter(l => l.code !== sourceLanguage).map(lang => (
828
+ <div key={lang.code} className="space-y-2">
829
+ <label className="text-sm font-medium flex items-center gap-2">
830
+ {lang.flag && <span>{lang.flag}</span>}
831
+ {lang.label}
832
+ </label>
833
+ <Textarea
834
+ value={selectedEntry.translations[lang.code]?.value || ""}
835
+ onChange={e => {
836
+ handleTranslationChange(selectedEntry.key, lang.code, e.target.value)
837
+ setSelectedEntry({
838
+ ...selectedEntry,
839
+ translations: {
840
+ ...selectedEntry.translations,
841
+ [lang.code]: {
842
+ ...selectedEntry.translations[lang.code],
843
+ value: e.target.value,
844
+ },
845
+ },
846
+ })
847
+ }}
848
+ placeholder={`Traduction ${lang.label}...`}
849
+ className="min-h-[100px]"
850
+ />
851
+ </div>
852
+ ))}
853
+
854
+ {/* Comment */}
855
+ <div className="space-y-2">
856
+ <label className="text-sm font-medium">Commentaire (optionnel)</label>
857
+ <Textarea
858
+ placeholder="Contexte ou indication pour la traduction..."
859
+ className="min-h-[80px]"
860
+ />
861
+ </div>
862
+ </div>
863
+ )}
864
+
865
+ <SheetFooter>
866
+ <Button variant="outline" onClick={() => setIsEditorOpen(false)}>
867
+ Fermer
868
+ </Button>
869
+ </SheetFooter>
870
+ </SheetContent>
871
+ </Sheet>
872
+
873
+ {/* Add Key Dialog */}
874
+ <Dialog open={isAddKeyOpen} onOpenChange={setIsAddKeyOpen}>
875
+ <DialogContent>
876
+ <DialogHeader>
877
+ <DialogTitle>Ajouter une clé de traduction</DialogTitle>
878
+ <DialogDescription>
879
+ Entrez la clé de traduction (ex: common.buttons.submit)
880
+ </DialogDescription>
881
+ </DialogHeader>
882
+
883
+ <div className="py-4">
884
+ <Input
885
+ value={newKey}
886
+ onChange={e => setNewKey(e.target.value)}
887
+ placeholder="namespace.key.name"
888
+ />
889
+ </div>
890
+
891
+ <DialogFooter>
892
+ <Button variant="outline" onClick={() => setIsAddKeyOpen(false)}>
893
+ Annuler
894
+ </Button>
895
+ <Button onClick={handleAddKey}>
896
+ Ajouter
897
+ </Button>
898
+ </DialogFooter>
899
+ </DialogContent>
900
+ </Dialog>
901
+
902
+ {/* Add Language Dialog */}
903
+ <Dialog open={isAddLanguageOpen} onOpenChange={setIsAddLanguageOpen}>
904
+ <DialogContent>
905
+ <DialogHeader>
906
+ <DialogTitle>Ajouter une langue</DialogTitle>
907
+ <DialogDescription>
908
+ Configurez la nouvelle langue à ajouter
909
+ </DialogDescription>
910
+ </DialogHeader>
911
+
912
+ <div className="py-4 space-y-4">
913
+ <div className="space-y-2">
914
+ <label className="text-sm font-medium">Code (ex: de, it, pt)</label>
915
+ <Input
916
+ value={newLanguage.code}
917
+ onChange={e => setNewLanguage({ ...newLanguage, code: e.target.value })}
918
+ placeholder="de"
919
+ />
920
+ </div>
921
+ <div className="space-y-2">
922
+ <label className="text-sm font-medium">Label</label>
923
+ <Input
924
+ value={newLanguage.label}
925
+ onChange={e => setNewLanguage({ ...newLanguage, label: e.target.value })}
926
+ placeholder="Allemand"
927
+ />
928
+ </div>
929
+ <div className="space-y-2">
930
+ <label className="text-sm font-medium">Emoji drapeau (optionnel)</label>
931
+ <Input
932
+ value={newLanguage.flag}
933
+ onChange={e => setNewLanguage({ ...newLanguage, flag: e.target.value })}
934
+ placeholder="🇩🇪"
935
+ />
936
+ </div>
937
+ </div>
938
+
939
+ <DialogFooter>
940
+ <Button variant="outline" onClick={() => setIsAddLanguageOpen(false)}>
941
+ Annuler
942
+ </Button>
943
+ <Button onClick={handleAddLanguage}>
944
+ Ajouter
945
+ </Button>
946
+ </DialogFooter>
947
+ </DialogContent>
948
+ </Dialog>
949
+
950
+ {/* Import Dialog */}
951
+ <Dialog open={isImportOpen} onOpenChange={setIsImportOpen}>
952
+ <DialogContent>
953
+ <DialogHeader>
954
+ <DialogTitle>Importer des traductions</DialogTitle>
955
+ <DialogDescription>
956
+ Sélectionnez un fichier JSON à importer
957
+ </DialogDescription>
958
+ </DialogHeader>
959
+
960
+ <div className="py-4 space-y-4">
961
+ <div className="space-y-2">
962
+ <label className="text-sm font-medium">Fichier JSON</label>
963
+ <Input
964
+ type="file"
965
+ accept=".json"
966
+ onChange={e => setImportFile(e.target.files?.[0] || null)}
967
+ />
968
+ </div>
969
+
970
+ <div className="space-y-2">
971
+ <label className="text-sm font-medium">Langue cible</label>
972
+ <Select value={importLanguage} onValueChange={setImportLanguage}>
973
+ <SelectTrigger>
974
+ <SelectValue placeholder="Sélectionner une langue" />
975
+ </SelectTrigger>
976
+ <SelectContent>
977
+ {languages.map(lang => (
978
+ <SelectItem key={lang.code} value={lang.code}>
979
+ {lang.flag && <span className="mr-2">{lang.flag}</span>}
980
+ {lang.label}
981
+ </SelectItem>
982
+ ))}
983
+ </SelectContent>
984
+ </Select>
985
+ </div>
986
+
987
+ <div className="space-y-2">
988
+ <label className="text-sm font-medium">Stratégie</label>
989
+ <Select value={importStrategy} onValueChange={(v) => setImportStrategy(v as "overwrite" | "merge")}>
990
+ <SelectTrigger>
991
+ <SelectValue />
992
+ </SelectTrigger>
993
+ <SelectContent>
994
+ <SelectItem value="merge">Fusionner (conserver les existantes)</SelectItem>
995
+ <SelectItem value="overwrite">Écraser tout</SelectItem>
996
+ </SelectContent>
997
+ </Select>
998
+ </div>
999
+ </div>
1000
+
1001
+ <DialogFooter>
1002
+ <Button variant="outline" onClick={() => setIsImportOpen(false)}>
1003
+ Annuler
1004
+ </Button>
1005
+ <Button onClick={handleImport} disabled={!importFile || !importLanguage}>
1006
+ Importer
1007
+ </Button>
1008
+ </DialogFooter>
1009
+ </DialogContent>
1010
+ </Dialog>
1011
+ </div>
1012
+ </TooltipProvider>
1013
+ )
1014
+ }
1015
+
1016
+ export default WakaI18nEditor