@medusajs/dashboard 3.0.0-snapshot-20251216135612 → 3.0.0-snapshot-20251216145629

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.
@@ -122,7 +122,9 @@
122
122
  },
123
123
  "actions": {
124
124
  "save": "Guardar",
125
+ "saveChanges": "Guardar cambios",
125
126
  "saveAsDraft": "Guardar como borrador",
127
+ "saveAndClose": "Guardar y cerrar",
126
128
  "copy": "Copiar",
127
129
  "copied": "Copiado",
128
130
  "duplicate": "Duplicar",
@@ -2460,13 +2462,23 @@
2460
2462
  "header": "Editor de Traducciones en Masa",
2461
2463
  "locale": "Idioma"
2462
2464
  },
2465
+ "edit": {
2466
+ "successToast": "Traducciones actualizadas exitosamente",
2467
+ "unsavedChanges": {
2468
+ "title": "Traducciones sin guardar",
2469
+ "description": "No pierdas tu trabajo. Tienes cambios que no han sido guardados aún"
2470
+ }
2471
+ },
2463
2472
  "activeLocales": {
2464
2473
  "heading": "Idiomas",
2465
2474
  "subtitle": "Traducciones activas",
2466
2475
  "noLocalesTip": "Configura al menos un idioma para empezar a traducir tu información"
2467
2476
  },
2468
2477
  "completion": {
2469
- "heading": "Textos traducidos"
2478
+ "heading": "Textos traducidos",
2479
+ "translated": "Traducidos",
2480
+ "toTranslate": "Por traducir",
2481
+ "footer": "Idiomas"
2470
2482
  }
2471
2483
  },
2472
2484
  "store": {
@@ -1,15 +1,28 @@
1
- import { AdminTranslationEntityStatistics } from "@medusajs/types"
2
- import { Container, Heading, Text } from "@medusajs/ui"
1
+ import { AdminTranslationEntityStatistics, HttpTypes } from "@medusajs/types"
2
+ import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
3
+ import { useMemo, useState } from "react"
3
4
  import { useTranslation } from "react-i18next"
4
5
 
5
6
  type TranslationsCompletionSectionProps = {
6
7
  statistics: Record<string, AdminTranslationEntityStatistics>
8
+ locales: HttpTypes.AdminLocale[]
9
+ }
10
+
11
+ type LocaleStats = {
12
+ code: string
13
+ name: string
14
+ translated: number
15
+ toTranslate: number
16
+ total: number
7
17
  }
8
18
 
9
19
  export const TranslationsCompletionSection = ({
10
20
  statistics,
21
+ locales,
11
22
  }: TranslationsCompletionSectionProps) => {
12
23
  const { t } = useTranslation()
24
+ const [hoveredLocale, setHoveredLocale] = useState<string | null>(null)
25
+
13
26
  const { translatedCount, totalCount } = Object.values(statistics).reduce(
14
27
  (acc, curr) => ({
15
28
  translatedCount: acc.translatedCount + curr.translated,
@@ -21,6 +34,47 @@ export const TranslationsCompletionSection = ({
21
34
  const percentage = totalCount > 0 ? (translatedCount / totalCount) * 100 : 0
22
35
  const remaining = Math.max(0, totalCount - translatedCount)
23
36
 
37
+ const localeStats = useMemo((): LocaleStats[] => {
38
+ const localeMap = new Map<
39
+ string,
40
+ { translated: number; expected: number }
41
+ >()
42
+
43
+ locales.forEach((locale) => {
44
+ localeMap.set(locale.code, { translated: 0, expected: 0 })
45
+ })
46
+
47
+ Object.values(statistics).forEach((entityStats) => {
48
+ if (entityStats.by_locale) {
49
+ Object.entries(entityStats.by_locale).forEach(
50
+ ([localeCode, localeData]) => {
51
+ const existing = localeMap.get(localeCode)
52
+ if (existing) {
53
+ existing.translated += localeData.translated
54
+ existing.expected += localeData.expected
55
+ }
56
+ }
57
+ )
58
+ }
59
+ })
60
+
61
+ return locales.map((locale) => {
62
+ const stats = localeMap.get(locale.code) || { translated: 0, expected: 0 }
63
+ return {
64
+ code: locale.code,
65
+ name: locale.name,
66
+ translated: stats.translated,
67
+ toTranslate: Math.max(0, stats.expected - stats.translated),
68
+ total: stats.expected,
69
+ }
70
+ })
71
+ }, [statistics, locales])
72
+
73
+ const maxTotal = useMemo(
74
+ () => Math.max(...localeStats.map((s) => s.total), 1),
75
+ [localeStats]
76
+ )
77
+
24
78
  return (
25
79
  <Container className="flex flex-col gap-y-3 px-6 py-4">
26
80
  <div className="flex items-center justify-between">
@@ -68,6 +122,103 @@ export const TranslationsCompletionSection = ({
68
122
  {remaining.toLocaleString()} {t("general.remaining").toLowerCase()}
69
123
  </Text>
70
124
  </div>
125
+
126
+ {localeStats.length > 0 && (
127
+ <div className="mt-4 flex flex-col gap-y-2">
128
+ <div className="flex h-32 w-full items-end gap-1">
129
+ {localeStats.map((locale) => {
130
+ const heightPercent = (locale.total / maxTotal) * 100
131
+ const translatedPercent =
132
+ locale.total > 0 ? (locale.translated / locale.total) * 100 : 0
133
+
134
+ return (
135
+ <Tooltip
136
+ key={locale.code}
137
+ open={hoveredLocale === locale.code}
138
+ content={
139
+ <div className="flex flex-col gap-y-1 p-1">
140
+ <Text size="small" weight="plus">
141
+ {locale.name}
142
+ </Text>
143
+ <div className="flex items-center justify-between">
144
+ <div className="flex items-center gap-x-2">
145
+ <div
146
+ className="h-2 w-2 rounded-full"
147
+ style={{ backgroundColor: "var(--bg-interactive)" }}
148
+ />
149
+ <Text
150
+ size="small"
151
+ weight="plus"
152
+ className="text-ui-fg-subtle"
153
+ >
154
+ {t("translations.completion.translated")}
155
+ </Text>
156
+ </div>
157
+ <Text size="small" weight="plus">
158
+ {locale.translated}
159
+ </Text>
160
+ </div>
161
+ <div className="flex items-center gap-x-2">
162
+ <div
163
+ className="h-2 w-2 rounded-full"
164
+ style={{
165
+ backgroundColor: "var(--bg-interactive)",
166
+ opacity: 0.3,
167
+ }}
168
+ />
169
+ <Text
170
+ size="small"
171
+ weight="plus"
172
+ className="text-ui-fg-subtle"
173
+ >
174
+ {t("translations.completion.toTranslate")}
175
+ </Text>
176
+ <Text size="small" weight="plus">
177
+ {locale.toTranslate}
178
+ </Text>
179
+ </div>
180
+ </div>
181
+ }
182
+ >
183
+ <div
184
+ className="flex min-w-2 flex-1 cursor-pointer flex-col justify-end overflow-hidden rounded-t-sm transition-opacity"
185
+ style={{ height: `${heightPercent}%` }}
186
+ onMouseEnter={() => setHoveredLocale(locale.code)}
187
+ onMouseLeave={() => setHoveredLocale(null)}
188
+ >
189
+ <div
190
+ className="w-full rounded-t-sm"
191
+ style={{
192
+ height: `${100 - translatedPercent}%`,
193
+ backgroundColor: "var(--bg-interactive)",
194
+ opacity: 0.3,
195
+ minHeight: locale.toTranslate > 0 ? "2px" : "0",
196
+ }}
197
+ />
198
+ {translatedPercent > 0 && (
199
+ <div
200
+ className="mt-0.5 w-full rounded-sm"
201
+ style={{
202
+ height: `${translatedPercent}%`,
203
+ backgroundColor: "var(--bg-interactive)",
204
+ minHeight: locale.translated > 0 ? "2px" : "0",
205
+ }}
206
+ />
207
+ )}
208
+ </div>
209
+ </Tooltip>
210
+ )
211
+ })}
212
+ </div>
213
+ <Text
214
+ size="small"
215
+ weight="plus"
216
+ className="text-ui-fg-muted text-center"
217
+ >
218
+ {t("translations.completion.footer")}
219
+ </Text>
220
+ </div>
221
+ )}
71
222
  </Container>
72
223
  )
73
224
  }
@@ -1,7 +1,6 @@
1
1
  import { Container, Heading, Text } from "@medusajs/ui"
2
2
  import { TwoColumnPage } from "../../../components/layout/pages"
3
3
  import { useTranslation } from "react-i18next"
4
- import { Buildings } from "@medusajs/icons"
5
4
  import {
6
5
  useStore,
7
6
  useTranslationSettings,
@@ -125,7 +124,14 @@ export const TranslationList = () => {
125
124
  ) ?? []
126
125
  }
127
126
  ></ActiveLocalesSection>
128
- <TranslationsCompletionSection statistics={statistics ?? {}} />
127
+ <TranslationsCompletionSection
128
+ statistics={statistics ?? {}}
129
+ locales={
130
+ store?.supported_locales?.map(
131
+ (supportedLocale) => supportedLocale.locale
132
+ ) ?? []
133
+ }
134
+ />
129
135
  </TwoColumnPage.Sidebar>
130
136
  </TwoColumnPage>
131
137
  )
@@ -426,7 +426,7 @@ export const TranslationsEditForm = ({
426
426
  const calculateColumnWidth = () => {
427
427
  if (containerRef.current) {
428
428
  const containerWidth = containerRef.current.offsetWidth
429
- const availableWidth = containerWidth - FIELD_COLUMN_WIDTH - 12
429
+ const availableWidth = containerWidth - FIELD_COLUMN_WIDTH - 16
430
430
  const columnWidth = Math.max(300, Math.floor(availableWidth / 2))
431
431
  setDynamicColumnWidth(columnWidth)
432
432
  }
@@ -588,11 +588,7 @@ export const TranslationsEditForm = ({
588
588
  async (closeOnSuccess: boolean = false) => {
589
589
  const success = await saveCurrentLocale()
590
590
  if (success) {
591
- toast.success(
592
- t("translations.edit.successToast", {
593
- defaultValue: "Translations updated successfully",
594
- })
595
- )
591
+ toast.success(t("translations.edit.successToast"))
596
592
  if (closeOnSuccess) {
597
593
  handleSuccess()
598
594
  }
@@ -613,9 +609,6 @@ export const TranslationsEditForm = ({
613
609
  payload.update.length === 0 &&
614
610
  payload.delete.length === 0
615
611
  ) {
616
- toast.info(
617
- t("translations.noChanges", { defaultValue: "No changes to save" })
618
- )
619
612
  return
620
613
  }
621
614
 
@@ -736,7 +729,7 @@ export const TranslationsEditForm = ({
736
729
  onClick={() => handleSave(false)}
737
730
  isLoading={isPending}
738
731
  >
739
- {t("actions.saveChanges", { defaultValue: "Save changes" })}
732
+ {t("actions.saveChanges")}
740
733
  </Button>
741
734
  <Button
742
735
  size="small"
@@ -744,7 +737,7 @@ export const TranslationsEditForm = ({
744
737
  onClick={() => handleSave(true)}
745
738
  isLoading={isPending}
746
739
  >
747
- {t("actions.saveAndClose", { defaultValue: "Save and close" })}
740
+ {t("actions.saveAndClose")}
748
741
  </Button>
749
742
  </div>
750
743
  </RouteFocusModal.Footer>
@@ -754,15 +747,10 @@ export const TranslationsEditForm = ({
754
747
  <Prompt.Content>
755
748
  <Prompt.Header>
756
749
  <Prompt.Title>
757
- {t("translations.unsavedChanges.title", {
758
- defaultValue: "Unsaved changes",
759
- })}
750
+ {t("translations.edit.unsavedChanges.title")}
760
751
  </Prompt.Title>
761
752
  <Prompt.Description>
762
- {t("translations.unsavedChanges.description", {
763
- defaultValue:
764
- "You have unsaved changes for this locale. Save them before switching?",
765
- })}
753
+ {t("translations.edit.unsavedChanges.description")}
766
754
  </Prompt.Description>
767
755
  </Prompt.Header>
768
756
  <Prompt.Footer>
@@ -780,7 +768,7 @@ export const TranslationsEditForm = ({
780
768
  type="button"
781
769
  isLoading={isPending}
782
770
  >
783
- {t("actions.saveChanges", { defaultValue: "Save changes" })}
771
+ {t("actions.saveChanges")}
784
772
  </Button>
785
773
  </Prompt.Footer>
786
774
  </Prompt.Content>