diff --git a/backend/src/main/java/com/printcalculator/controller/OptionsController.java b/backend/src/main/java/com/printcalculator/controller/OptionsController.java index 28a1abb..d4f65e1 100644 --- a/backend/src/main/java/com/printcalculator/controller/OptionsController.java +++ b/backend/src/main/java/com/printcalculator/controller/OptionsController.java @@ -94,6 +94,10 @@ public class OptionsController { v.getId(), v.getVariantDisplayName(), v.getColorName(), + v.getColorLabelIt(), + v.getColorLabelEn(), + v.getColorLabelDe(), + v.getColorLabelFr(), resolveHexColor(v), v.getFinishType() != null ? v.getFinishType() : "GLOSSY", v.getStockSpools() != null ? v.getStockSpools().doubleValue() : 0d, diff --git a/backend/src/main/java/com/printcalculator/dto/AdminFilamentVariantDto.java b/backend/src/main/java/com/printcalculator/dto/AdminFilamentVariantDto.java index 88b32ac..f5cb9a3 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminFilamentVariantDto.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminFilamentVariantDto.java @@ -12,6 +12,10 @@ public class AdminFilamentVariantDto { private String materialTechnicalTypeLabel; private String variantDisplayName; private String colorName; + private String colorLabelIt; + private String colorLabelEn; + private String colorLabelDe; + private String colorLabelFr; private String colorHex; private String finishType; private String brand; @@ -89,6 +93,38 @@ public class AdminFilamentVariantDto { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } diff --git a/backend/src/main/java/com/printcalculator/dto/AdminShopCategoryDto.java b/backend/src/main/java/com/printcalculator/dto/AdminShopCategoryDto.java index 3e43c0d..a61c326 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminShopCategoryDto.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminShopCategoryDto.java @@ -10,9 +10,25 @@ public class AdminShopCategoryDto { private String parentCategoryName; private String slug; private String name; + private String nameIt; + private String nameEn; + private String nameDe; + private String nameFr; private String description; + private String descriptionIt; + private String descriptionEn; + private String descriptionDe; + private String descriptionFr; private String seoTitle; + private String seoTitleIt; + private String seoTitleEn; + private String seoTitleDe; + private String seoTitleFr; private String seoDescription; + private String seoDescriptionIt; + private String seoDescriptionEn; + private String seoDescriptionDe; + private String seoDescriptionFr; private String ogTitle; private String ogDescription; private Boolean indexable; @@ -69,6 +85,38 @@ public class AdminShopCategoryDto { this.name = name; } + public String getNameIt() { + return nameIt; + } + + public void setNameIt(String nameIt) { + this.nameIt = nameIt; + } + + public String getNameEn() { + return nameEn; + } + + public void setNameEn(String nameEn) { + this.nameEn = nameEn; + } + + public String getNameDe() { + return nameDe; + } + + public void setNameDe(String nameDe) { + this.nameDe = nameDe; + } + + public String getNameFr() { + return nameFr; + } + + public void setNameFr(String nameFr) { + this.nameFr = nameFr; + } + public String getDescription() { return description; } @@ -77,6 +125,38 @@ public class AdminShopCategoryDto { this.description = description; } + public String getDescriptionIt() { + return descriptionIt; + } + + public void setDescriptionIt(String descriptionIt) { + this.descriptionIt = descriptionIt; + } + + public String getDescriptionEn() { + return descriptionEn; + } + + public void setDescriptionEn(String descriptionEn) { + this.descriptionEn = descriptionEn; + } + + public String getDescriptionDe() { + return descriptionDe; + } + + public void setDescriptionDe(String descriptionDe) { + this.descriptionDe = descriptionDe; + } + + public String getDescriptionFr() { + return descriptionFr; + } + + public void setDescriptionFr(String descriptionFr) { + this.descriptionFr = descriptionFr; + } + public String getSeoTitle() { return seoTitle; } @@ -85,6 +165,38 @@ public class AdminShopCategoryDto { this.seoTitle = seoTitle; } + public String getSeoTitleIt() { + return seoTitleIt; + } + + public void setSeoTitleIt(String seoTitleIt) { + this.seoTitleIt = seoTitleIt; + } + + public String getSeoTitleEn() { + return seoTitleEn; + } + + public void setSeoTitleEn(String seoTitleEn) { + this.seoTitleEn = seoTitleEn; + } + + public String getSeoTitleDe() { + return seoTitleDe; + } + + public void setSeoTitleDe(String seoTitleDe) { + this.seoTitleDe = seoTitleDe; + } + + public String getSeoTitleFr() { + return seoTitleFr; + } + + public void setSeoTitleFr(String seoTitleFr) { + this.seoTitleFr = seoTitleFr; + } + public String getSeoDescription() { return seoDescription; } @@ -93,6 +205,38 @@ public class AdminShopCategoryDto { this.seoDescription = seoDescription; } + public String getSeoDescriptionIt() { + return seoDescriptionIt; + } + + public void setSeoDescriptionIt(String seoDescriptionIt) { + this.seoDescriptionIt = seoDescriptionIt; + } + + public String getSeoDescriptionEn() { + return seoDescriptionEn; + } + + public void setSeoDescriptionEn(String seoDescriptionEn) { + this.seoDescriptionEn = seoDescriptionEn; + } + + public String getSeoDescriptionDe() { + return seoDescriptionDe; + } + + public void setSeoDescriptionDe(String seoDescriptionDe) { + this.seoDescriptionDe = seoDescriptionDe; + } + + public String getSeoDescriptionFr() { + return seoDescriptionFr; + } + + public void setSeoDescriptionFr(String seoDescriptionFr) { + this.seoDescriptionFr = seoDescriptionFr; + } + public String getOgTitle() { return ogTitle; } diff --git a/backend/src/main/java/com/printcalculator/dto/AdminShopProductVariantDto.java b/backend/src/main/java/com/printcalculator/dto/AdminShopProductVariantDto.java index e03c629..9a32330 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminShopProductVariantDto.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminShopProductVariantDto.java @@ -9,6 +9,10 @@ public class AdminShopProductVariantDto { private String sku; private String variantLabel; private String colorName; + private String colorLabelIt; + private String colorLabelEn; + private String colorLabelDe; + private String colorLabelFr; private String colorHex; private String internalMaterialCode; private BigDecimal priceChf; @@ -50,6 +54,38 @@ public class AdminShopProductVariantDto { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } diff --git a/backend/src/main/java/com/printcalculator/dto/AdminUpsertFilamentVariantRequest.java b/backend/src/main/java/com/printcalculator/dto/AdminUpsertFilamentVariantRequest.java index 89cd51c..820141a 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminUpsertFilamentVariantRequest.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminUpsertFilamentVariantRequest.java @@ -6,6 +6,10 @@ public class AdminUpsertFilamentVariantRequest { private Long materialTypeId; private String variantDisplayName; private String colorName; + private String colorLabelIt; + private String colorLabelEn; + private String colorLabelDe; + private String colorLabelFr; private String colorHex; private String finishType; private String brand; @@ -40,6 +44,38 @@ public class AdminUpsertFilamentVariantRequest { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } diff --git a/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopCategoryRequest.java b/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopCategoryRequest.java index 28096f2..a8ed10f 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopCategoryRequest.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopCategoryRequest.java @@ -6,9 +6,25 @@ public class AdminUpsertShopCategoryRequest { private UUID parentCategoryId; private String slug; private String name; + private String nameIt; + private String nameEn; + private String nameDe; + private String nameFr; private String description; + private String descriptionIt; + private String descriptionEn; + private String descriptionDe; + private String descriptionFr; private String seoTitle; + private String seoTitleIt; + private String seoTitleEn; + private String seoTitleDe; + private String seoTitleFr; private String seoDescription; + private String seoDescriptionIt; + private String seoDescriptionEn; + private String seoDescriptionDe; + private String seoDescriptionFr; private String ogTitle; private String ogDescription; private Boolean indexable; @@ -39,6 +55,38 @@ public class AdminUpsertShopCategoryRequest { this.name = name; } + public String getNameIt() { + return nameIt; + } + + public void setNameIt(String nameIt) { + this.nameIt = nameIt; + } + + public String getNameEn() { + return nameEn; + } + + public void setNameEn(String nameEn) { + this.nameEn = nameEn; + } + + public String getNameDe() { + return nameDe; + } + + public void setNameDe(String nameDe) { + this.nameDe = nameDe; + } + + public String getNameFr() { + return nameFr; + } + + public void setNameFr(String nameFr) { + this.nameFr = nameFr; + } + public String getDescription() { return description; } @@ -47,6 +95,38 @@ public class AdminUpsertShopCategoryRequest { this.description = description; } + public String getDescriptionIt() { + return descriptionIt; + } + + public void setDescriptionIt(String descriptionIt) { + this.descriptionIt = descriptionIt; + } + + public String getDescriptionEn() { + return descriptionEn; + } + + public void setDescriptionEn(String descriptionEn) { + this.descriptionEn = descriptionEn; + } + + public String getDescriptionDe() { + return descriptionDe; + } + + public void setDescriptionDe(String descriptionDe) { + this.descriptionDe = descriptionDe; + } + + public String getDescriptionFr() { + return descriptionFr; + } + + public void setDescriptionFr(String descriptionFr) { + this.descriptionFr = descriptionFr; + } + public String getSeoTitle() { return seoTitle; } @@ -55,6 +135,38 @@ public class AdminUpsertShopCategoryRequest { this.seoTitle = seoTitle; } + public String getSeoTitleIt() { + return seoTitleIt; + } + + public void setSeoTitleIt(String seoTitleIt) { + this.seoTitleIt = seoTitleIt; + } + + public String getSeoTitleEn() { + return seoTitleEn; + } + + public void setSeoTitleEn(String seoTitleEn) { + this.seoTitleEn = seoTitleEn; + } + + public String getSeoTitleDe() { + return seoTitleDe; + } + + public void setSeoTitleDe(String seoTitleDe) { + this.seoTitleDe = seoTitleDe; + } + + public String getSeoTitleFr() { + return seoTitleFr; + } + + public void setSeoTitleFr(String seoTitleFr) { + this.seoTitleFr = seoTitleFr; + } + public String getSeoDescription() { return seoDescription; } @@ -63,6 +175,38 @@ public class AdminUpsertShopCategoryRequest { this.seoDescription = seoDescription; } + public String getSeoDescriptionIt() { + return seoDescriptionIt; + } + + public void setSeoDescriptionIt(String seoDescriptionIt) { + this.seoDescriptionIt = seoDescriptionIt; + } + + public String getSeoDescriptionEn() { + return seoDescriptionEn; + } + + public void setSeoDescriptionEn(String seoDescriptionEn) { + this.seoDescriptionEn = seoDescriptionEn; + } + + public String getSeoDescriptionDe() { + return seoDescriptionDe; + } + + public void setSeoDescriptionDe(String seoDescriptionDe) { + this.seoDescriptionDe = seoDescriptionDe; + } + + public String getSeoDescriptionFr() { + return seoDescriptionFr; + } + + public void setSeoDescriptionFr(String seoDescriptionFr) { + this.seoDescriptionFr = seoDescriptionFr; + } + public String getOgTitle() { return ogTitle; } diff --git a/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopProductVariantRequest.java b/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopProductVariantRequest.java index 14ef9af..2b84871 100644 --- a/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopProductVariantRequest.java +++ b/backend/src/main/java/com/printcalculator/dto/AdminUpsertShopProductVariantRequest.java @@ -8,6 +8,10 @@ public class AdminUpsertShopProductVariantRequest { private String sku; private String variantLabel; private String colorName; + private String colorLabelIt; + private String colorLabelEn; + private String colorLabelDe; + private String colorLabelFr; private String colorHex; private String internalMaterialCode; private BigDecimal priceChf; @@ -47,6 +51,38 @@ public class AdminUpsertShopProductVariantRequest { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } diff --git a/backend/src/main/java/com/printcalculator/dto/OptionsResponse.java b/backend/src/main/java/com/printcalculator/dto/OptionsResponse.java index 9b85460..566c366 100644 --- a/backend/src/main/java/com/printcalculator/dto/OptionsResponse.java +++ b/backend/src/main/java/com/printcalculator/dto/OptionsResponse.java @@ -15,6 +15,10 @@ public record OptionsResponse( Long id, String name, String colorName, + String colorLabelIt, + String colorLabelEn, + String colorLabelDe, + String colorLabelFr, String hexColor, String finishType, Double stockSpools, diff --git a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java index efbcc87..8c21d5e 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderItemDto.java @@ -17,9 +17,17 @@ public class OrderItemDto { private String shopProductName; private String shopVariantLabel; private String shopVariantColorName; + private String shopVariantColorLabelIt; + private String shopVariantColorLabelEn; + private String shopVariantColorLabelDe; + private String shopVariantColorLabelFr; private String shopVariantColorHex; private String filamentVariantDisplayName; private String filamentColorName; + private String filamentColorLabelIt; + private String filamentColorLabelEn; + private String filamentColorLabelDe; + private String filamentColorLabelFr; private String filamentColorHex; private String quality; private BigDecimal nozzleDiameterMm; @@ -73,6 +81,18 @@ public class OrderItemDto { public String getShopVariantColorName() { return shopVariantColorName; } public void setShopVariantColorName(String shopVariantColorName) { this.shopVariantColorName = shopVariantColorName; } + public String getShopVariantColorLabelIt() { return shopVariantColorLabelIt; } + public void setShopVariantColorLabelIt(String shopVariantColorLabelIt) { this.shopVariantColorLabelIt = shopVariantColorLabelIt; } + + public String getShopVariantColorLabelEn() { return shopVariantColorLabelEn; } + public void setShopVariantColorLabelEn(String shopVariantColorLabelEn) { this.shopVariantColorLabelEn = shopVariantColorLabelEn; } + + public String getShopVariantColorLabelDe() { return shopVariantColorLabelDe; } + public void setShopVariantColorLabelDe(String shopVariantColorLabelDe) { this.shopVariantColorLabelDe = shopVariantColorLabelDe; } + + public String getShopVariantColorLabelFr() { return shopVariantColorLabelFr; } + public void setShopVariantColorLabelFr(String shopVariantColorLabelFr) { this.shopVariantColorLabelFr = shopVariantColorLabelFr; } + public String getShopVariantColorHex() { return shopVariantColorHex; } public void setShopVariantColorHex(String shopVariantColorHex) { this.shopVariantColorHex = shopVariantColorHex; } @@ -82,6 +102,18 @@ public class OrderItemDto { public String getFilamentColorName() { return filamentColorName; } public void setFilamentColorName(String filamentColorName) { this.filamentColorName = filamentColorName; } + public String getFilamentColorLabelIt() { return filamentColorLabelIt; } + public void setFilamentColorLabelIt(String filamentColorLabelIt) { this.filamentColorLabelIt = filamentColorLabelIt; } + + public String getFilamentColorLabelEn() { return filamentColorLabelEn; } + public void setFilamentColorLabelEn(String filamentColorLabelEn) { this.filamentColorLabelEn = filamentColorLabelEn; } + + public String getFilamentColorLabelDe() { return filamentColorLabelDe; } + public void setFilamentColorLabelDe(String filamentColorLabelDe) { this.filamentColorLabelDe = filamentColorLabelDe; } + + public String getFilamentColorLabelFr() { return filamentColorLabelFr; } + public void setFilamentColorLabelFr(String filamentColorLabelFr) { this.filamentColorLabelFr = filamentColorLabelFr; } + public String getFilamentColorHex() { return filamentColorHex; } public void setFilamentColorHex(String filamentColorHex) { this.filamentColorHex = filamentColorHex; } diff --git a/backend/src/main/java/com/printcalculator/dto/ShopProductVariantOptionDto.java b/backend/src/main/java/com/printcalculator/dto/ShopProductVariantOptionDto.java index 318a87c..c959bb4 100644 --- a/backend/src/main/java/com/printcalculator/dto/ShopProductVariantOptionDto.java +++ b/backend/src/main/java/com/printcalculator/dto/ShopProductVariantOptionDto.java @@ -8,6 +8,7 @@ public record ShopProductVariantOptionDto( String sku, String variantLabel, String colorName, + String colorLabel, String colorHex, BigDecimal priceChf, Boolean isDefault diff --git a/backend/src/main/java/com/printcalculator/entity/FilamentVariant.java b/backend/src/main/java/com/printcalculator/entity/FilamentVariant.java index e2f8bf5..465635e 100644 --- a/backend/src/main/java/com/printcalculator/entity/FilamentVariant.java +++ b/backend/src/main/java/com/printcalculator/entity/FilamentVariant.java @@ -24,6 +24,18 @@ public class FilamentVariant { @Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE) private String colorName; + @Column(name = "color_label_it", length = Integer.MAX_VALUE) + private String colorLabelIt; + + @Column(name = "color_label_en", length = Integer.MAX_VALUE) + private String colorLabelEn; + + @Column(name = "color_label_de", length = Integer.MAX_VALUE) + private String colorLabelDe; + + @Column(name = "color_label_fr", length = Integer.MAX_VALUE) + private String colorLabelFr; + @Column(name = "color_hex", length = Integer.MAX_VALUE) private String colorHex; @@ -93,6 +105,38 @@ public class FilamentVariant { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } @@ -173,4 +217,60 @@ public class FilamentVariant { this.createdAt = createdAt; } + public String getColorLabelForLanguage(String language) { + return resolveLocalizedValue( + language, + colorName, + colorLabelIt, + colorLabelEn, + colorLabelDe, + colorLabelFr + ); + } + + private String resolveLocalizedValue(String language, + String fallback, + String valueIt, + String valueEn, + String valueDe, + String valueFr) { + String normalizedLanguage = normalizeLanguage(language); + String preferred = switch (normalizedLanguage) { + case "it" -> valueIt; + case "en" -> valueEn; + case "de" -> valueDe; + case "fr" -> valueFr; + default -> null; + }; + String resolved = firstNonBlank(preferred, fallback); + if (resolved != null) { + return resolved; + } + return firstNonBlank(valueIt, valueEn, valueDe, valueFr); + } + + private String normalizeLanguage(String language) { + if (language == null) { + return ""; + } + String normalized = language.trim().toLowerCase(); + int separatorIndex = normalized.indexOf('-'); + if (separatorIndex > 0) { + normalized = normalized.substring(0, separatorIndex); + } + return normalized; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } + } diff --git a/backend/src/main/java/com/printcalculator/entity/ShopCategory.java b/backend/src/main/java/com/printcalculator/entity/ShopCategory.java index a018a97..87b4dd0 100644 --- a/backend/src/main/java/com/printcalculator/entity/ShopCategory.java +++ b/backend/src/main/java/com/printcalculator/entity/ShopCategory.java @@ -15,6 +15,7 @@ import jakarta.persistence.Table; import org.hibernate.annotations.ColumnDefault; import java.time.OffsetDateTime; +import java.util.List; import java.util.UUID; @Entity @@ -23,6 +24,8 @@ import java.util.UUID; @Index(name = "ix_shop_category_active_sort", columnList = "is_active, sort_order") }) public class ShopCategory { + public static final List SUPPORTED_LANGUAGES = List.of("it", "en", "de", "fr"); + @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "shop_category_id", nullable = false) @@ -38,15 +41,63 @@ public class ShopCategory { @Column(name = "name", nullable = false, length = Integer.MAX_VALUE) private String name; + @Column(name = "name_it", length = Integer.MAX_VALUE) + private String nameIt; + + @Column(name = "name_en", length = Integer.MAX_VALUE) + private String nameEn; + + @Column(name = "name_de", length = Integer.MAX_VALUE) + private String nameDe; + + @Column(name = "name_fr", length = Integer.MAX_VALUE) + private String nameFr; + @Column(name = "description", length = Integer.MAX_VALUE) private String description; + @Column(name = "description_it", length = Integer.MAX_VALUE) + private String descriptionIt; + + @Column(name = "description_en", length = Integer.MAX_VALUE) + private String descriptionEn; + + @Column(name = "description_de", length = Integer.MAX_VALUE) + private String descriptionDe; + + @Column(name = "description_fr", length = Integer.MAX_VALUE) + private String descriptionFr; + @Column(name = "seo_title", length = Integer.MAX_VALUE) private String seoTitle; + @Column(name = "seo_title_it", length = Integer.MAX_VALUE) + private String seoTitleIt; + + @Column(name = "seo_title_en", length = Integer.MAX_VALUE) + private String seoTitleEn; + + @Column(name = "seo_title_de", length = Integer.MAX_VALUE) + private String seoTitleDe; + + @Column(name = "seo_title_fr", length = Integer.MAX_VALUE) + private String seoTitleFr; + @Column(name = "seo_description", length = Integer.MAX_VALUE) private String seoDescription; + @Column(name = "seo_description_it", length = Integer.MAX_VALUE) + private String seoDescriptionIt; + + @Column(name = "seo_description_en", length = Integer.MAX_VALUE) + private String seoDescriptionEn; + + @Column(name = "seo_description_de", length = Integer.MAX_VALUE) + private String seoDescriptionDe; + + @Column(name = "seo_description_fr", length = Integer.MAX_VALUE) + private String seoDescriptionFr; + @Column(name = "og_title", length = Integer.MAX_VALUE) private String ogTitle; @@ -139,6 +190,38 @@ public class ShopCategory { this.name = name; } + public String getNameIt() { + return nameIt; + } + + public void setNameIt(String nameIt) { + this.nameIt = nameIt; + } + + public String getNameEn() { + return nameEn; + } + + public void setNameEn(String nameEn) { + this.nameEn = nameEn; + } + + public String getNameDe() { + return nameDe; + } + + public void setNameDe(String nameDe) { + this.nameDe = nameDe; + } + + public String getNameFr() { + return nameFr; + } + + public void setNameFr(String nameFr) { + this.nameFr = nameFr; + } + public String getDescription() { return description; } @@ -147,6 +230,38 @@ public class ShopCategory { this.description = description; } + public String getDescriptionIt() { + return descriptionIt; + } + + public void setDescriptionIt(String descriptionIt) { + this.descriptionIt = descriptionIt; + } + + public String getDescriptionEn() { + return descriptionEn; + } + + public void setDescriptionEn(String descriptionEn) { + this.descriptionEn = descriptionEn; + } + + public String getDescriptionDe() { + return descriptionDe; + } + + public void setDescriptionDe(String descriptionDe) { + this.descriptionDe = descriptionDe; + } + + public String getDescriptionFr() { + return descriptionFr; + } + + public void setDescriptionFr(String descriptionFr) { + this.descriptionFr = descriptionFr; + } + public String getSeoTitle() { return seoTitle; } @@ -155,6 +270,38 @@ public class ShopCategory { this.seoTitle = seoTitle; } + public String getSeoTitleIt() { + return seoTitleIt; + } + + public void setSeoTitleIt(String seoTitleIt) { + this.seoTitleIt = seoTitleIt; + } + + public String getSeoTitleEn() { + return seoTitleEn; + } + + public void setSeoTitleEn(String seoTitleEn) { + this.seoTitleEn = seoTitleEn; + } + + public String getSeoTitleDe() { + return seoTitleDe; + } + + public void setSeoTitleDe(String seoTitleDe) { + this.seoTitleDe = seoTitleDe; + } + + public String getSeoTitleFr() { + return seoTitleFr; + } + + public void setSeoTitleFr(String seoTitleFr) { + this.seoTitleFr = seoTitleFr; + } + public String getSeoDescription() { return seoDescription; } @@ -163,6 +310,38 @@ public class ShopCategory { this.seoDescription = seoDescription; } + public String getSeoDescriptionIt() { + return seoDescriptionIt; + } + + public void setSeoDescriptionIt(String seoDescriptionIt) { + this.seoDescriptionIt = seoDescriptionIt; + } + + public String getSeoDescriptionEn() { + return seoDescriptionEn; + } + + public void setSeoDescriptionEn(String seoDescriptionEn) { + this.seoDescriptionEn = seoDescriptionEn; + } + + public String getSeoDescriptionDe() { + return seoDescriptionDe; + } + + public void setSeoDescriptionDe(String seoDescriptionDe) { + this.seoDescriptionDe = seoDescriptionDe; + } + + public String getSeoDescriptionFr() { + return seoDescriptionFr; + } + + public void setSeoDescriptionFr(String seoDescriptionFr) { + this.seoDescriptionFr = seoDescriptionFr; + } + public String getOgTitle() { return ogTitle; } @@ -218,4 +397,109 @@ public class ShopCategory { public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; } + + public String getNameForLanguage(String language) { + return resolveLocalizedValue(language, name, nameIt, nameEn, nameDe, nameFr); + } + + public void setNameForLanguage(String language, String value) { + switch (normalizeLanguage(language)) { + case "it" -> nameIt = value; + case "en" -> nameEn = value; + case "de" -> nameDe = value; + case "fr" -> nameFr = value; + default -> { + } + } + } + + public String getDescriptionForLanguage(String language) { + return resolveLocalizedValue(language, description, descriptionIt, descriptionEn, descriptionDe, descriptionFr); + } + + public void setDescriptionForLanguage(String language, String value) { + switch (normalizeLanguage(language)) { + case "it" -> descriptionIt = value; + case "en" -> descriptionEn = value; + case "de" -> descriptionDe = value; + case "fr" -> descriptionFr = value; + default -> { + } + } + } + + public String getSeoTitleForLanguage(String language) { + return resolveLocalizedValue(language, seoTitle, seoTitleIt, seoTitleEn, seoTitleDe, seoTitleFr); + } + + public void setSeoTitleForLanguage(String language, String value) { + switch (normalizeLanguage(language)) { + case "it" -> seoTitleIt = value; + case "en" -> seoTitleEn = value; + case "de" -> seoTitleDe = value; + case "fr" -> seoTitleFr = value; + default -> { + } + } + } + + public String getSeoDescriptionForLanguage(String language) { + return resolveLocalizedValue(language, seoDescription, seoDescriptionIt, seoDescriptionEn, seoDescriptionDe, seoDescriptionFr); + } + + public void setSeoDescriptionForLanguage(String language, String value) { + switch (normalizeLanguage(language)) { + case "it" -> seoDescriptionIt = value; + case "en" -> seoDescriptionEn = value; + case "de" -> seoDescriptionDe = value; + case "fr" -> seoDescriptionFr = value; + default -> { + } + } + } + + private String resolveLocalizedValue(String language, + String fallback, + String valueIt, + String valueEn, + String valueDe, + String valueFr) { + String normalizedLanguage = normalizeLanguage(language); + String preferred = switch (normalizedLanguage) { + case "it" -> valueIt; + case "en" -> valueEn; + case "de" -> valueDe; + case "fr" -> valueFr; + default -> null; + }; + String resolved = firstNonBlank(preferred, fallback); + if (resolved != null) { + return resolved; + } + return firstNonBlank(valueIt, valueEn, valueDe, valueFr); + } + + private String normalizeLanguage(String language) { + if (language == null) { + return ""; + } + String normalized = language.trim().toLowerCase(); + int separatorIndex = normalized.indexOf('-'); + if (separatorIndex > 0) { + normalized = normalized.substring(0, separatorIndex); + } + return normalized; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } } diff --git a/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java b/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java index d1d6d03..24932a3 100644 --- a/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java +++ b/backend/src/main/java/com/printcalculator/entity/ShopProductVariant.java @@ -42,6 +42,18 @@ public class ShopProductVariant { @Column(name = "color_name", nullable = false, length = Integer.MAX_VALUE) private String colorName; + @Column(name = "color_label_it", length = Integer.MAX_VALUE) + private String colorLabelIt; + + @Column(name = "color_label_en", length = Integer.MAX_VALUE) + private String colorLabelEn; + + @Column(name = "color_label_de", length = Integer.MAX_VALUE) + private String colorLabelDe; + + @Column(name = "color_label_fr", length = Integer.MAX_VALUE) + private String colorLabelFr; + @Column(name = "color_hex", length = Integer.MAX_VALUE) private String colorHex; @@ -152,6 +164,38 @@ public class ShopProductVariant { this.colorName = colorName; } + public String getColorLabelIt() { + return colorLabelIt; + } + + public void setColorLabelIt(String colorLabelIt) { + this.colorLabelIt = colorLabelIt; + } + + public String getColorLabelEn() { + return colorLabelEn; + } + + public void setColorLabelEn(String colorLabelEn) { + this.colorLabelEn = colorLabelEn; + } + + public String getColorLabelDe() { + return colorLabelDe; + } + + public void setColorLabelDe(String colorLabelDe) { + this.colorLabelDe = colorLabelDe; + } + + public String getColorLabelFr() { + return colorLabelFr; + } + + public void setColorLabelFr(String colorLabelFr) { + this.colorLabelFr = colorLabelFr; + } + public String getColorHex() { return colorHex; } @@ -215,4 +259,60 @@ public class ShopProductVariant { public void setUpdatedAt(OffsetDateTime updatedAt) { this.updatedAt = updatedAt; } + + public String getColorLabelForLanguage(String language) { + return resolveLocalizedValue( + language, + colorName, + colorLabelIt, + colorLabelEn, + colorLabelDe, + colorLabelFr + ); + } + + private String resolveLocalizedValue(String language, + String fallback, + String valueIt, + String valueEn, + String valueDe, + String valueFr) { + String normalizedLanguage = normalizeLanguage(language); + String preferred = switch (normalizedLanguage) { + case "it" -> valueIt; + case "en" -> valueEn; + case "de" -> valueDe; + case "fr" -> valueFr; + default -> null; + }; + String resolved = firstNonBlank(preferred, fallback); + if (resolved != null) { + return resolved; + } + return firstNonBlank(valueIt, valueEn, valueDe, valueFr); + } + + private String normalizeLanguage(String language) { + if (language == null) { + return ""; + } + String normalized = language.trim().toLowerCase(); + int separatorIndex = normalized.indexOf('-'); + if (separatorIndex > 0) { + normalized = normalized.substring(0, separatorIndex); + } + return normalized; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } } diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminFilamentControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminFilamentControllerService.java index 1fc2de4..b540984 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminFilamentControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminFilamentControllerService.java @@ -161,10 +161,21 @@ public class AdminFilamentControllerService { String normalizedColorHex = normalizeAndValidateColorHex(payload.getColorHex()); String normalizedFinishType = normalizeAndValidateFinishType(payload.getFinishType(), payload.getIsMatte()); String normalizedBrand = normalizeOptional(payload.getBrand()); + String fallbackColorLabel = firstNonBlank( + normalizeOptional(payload.getColorLabelIt()), + normalizeOptional(payload.getColorLabelEn()), + normalizeOptional(payload.getColorLabelDe()), + normalizeOptional(payload.getColorLabelFr()), + normalizedColorName + ); variant.setFilamentMaterialType(material); variant.setVariantDisplayName(normalizedDisplayName); variant.setColorName(normalizedColorName); + variant.setColorLabelIt(firstNonBlank(normalizeOptional(payload.getColorLabelIt()), fallbackColorLabel)); + variant.setColorLabelEn(firstNonBlank(normalizeOptional(payload.getColorLabelEn()), fallbackColorLabel)); + variant.setColorLabelDe(firstNonBlank(normalizeOptional(payload.getColorLabelDe()), fallbackColorLabel)); + variant.setColorLabelFr(firstNonBlank(normalizeOptional(payload.getColorLabelFr()), fallbackColorLabel)); variant.setColorHex(normalizedColorHex); variant.setFinishType(normalizedFinishType); variant.setBrand(normalizedBrand); @@ -226,6 +237,18 @@ public class AdminFilamentControllerService { return normalized.isBlank() ? null : normalized; } + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } + private FilamentMaterialType validateAndResolveMaterial(AdminUpsertFilamentVariantRequest payload) { if (payload == null || payload.getMaterialTypeId() == null) { throw new ResponseStatusException(BAD_REQUEST, "Material type id is required"); @@ -306,6 +329,10 @@ public class AdminFilamentControllerService { dto.setVariantDisplayName(variant.getVariantDisplayName()); dto.setColorName(variant.getColorName()); + dto.setColorLabelIt(variant.getColorLabelIt()); + dto.setColorLabelEn(variant.getColorLabelEn()); + dto.setColorLabelDe(variant.getColorLabelDe()); + dto.setColorLabelFr(variant.getColorLabelFr()); dto.setColorHex(variant.getColorHex()); dto.setFinishType(variant.getFinishType()); dto.setBrand(variant.getBrand()); diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminShopCategoryControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminShopCategoryControllerService.java index e327ac6..e7665c9 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminShopCategoryControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminShopCategoryControllerService.java @@ -67,13 +67,13 @@ public class AdminShopCategoryControllerService { @Transactional public AdminShopCategoryDto createCategory(AdminUpsertShopCategoryRequest payload) { ensurePayload(payload); - String normalizedName = normalizeRequiredName(payload.getName()); - String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName); + LocalizedCategoryContent localizedContent = normalizeLocalizedCategoryContent(payload); + String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName()); ensureSlugAvailable(normalizedSlug, null); ShopCategory category = new ShopCategory(); category.setCreatedAt(OffsetDateTime.now()); - applyPayload(category, payload, normalizedName, normalizedSlug, null); + applyPayload(category, payload, localizedContent, normalizedSlug, null); ShopCategory saved = shopCategoryRepository.save(category); return getCategory(saved.getId()); @@ -86,11 +86,11 @@ public class AdminShopCategoryControllerService { ShopCategory category = shopCategoryRepository.findById(categoryId) .orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Shop category not found")); - String normalizedName = normalizeRequiredName(payload.getName()); - String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), normalizedName); + LocalizedCategoryContent localizedContent = normalizeLocalizedCategoryContent(payload); + String normalizedSlug = normalizeAndValidateSlug(payload.getSlug(), localizedContent.defaultName()); ensureSlugAvailable(normalizedSlug, category.getId()); - applyPayload(category, payload, normalizedName, normalizedSlug, category.getId()); + applyPayload(category, payload, localizedContent, normalizedSlug, category.getId()); ShopCategory saved = shopCategoryRepository.save(category); return getCategory(saved.getId()); } @@ -112,17 +112,33 @@ public class AdminShopCategoryControllerService { private void applyPayload(ShopCategory category, AdminUpsertShopCategoryRequest payload, - String normalizedName, + LocalizedCategoryContent localizedContent, String normalizedSlug, UUID currentCategoryId) { ShopCategory parentCategory = resolveParentCategory(payload.getParentCategoryId(), currentCategoryId); category.setParentCategory(parentCategory); category.setSlug(normalizedSlug); - category.setName(normalizedName); - category.setDescription(normalizeOptional(payload.getDescription())); - category.setSeoTitle(normalizeOptional(payload.getSeoTitle())); - category.setSeoDescription(normalizeOptional(payload.getSeoDescription())); + category.setName(localizedContent.defaultName()); + category.setNameIt(localizedContent.names().get("it")); + category.setNameEn(localizedContent.names().get("en")); + category.setNameDe(localizedContent.names().get("de")); + category.setNameFr(localizedContent.names().get("fr")); + category.setDescription(localizedContent.defaultDescription()); + category.setDescriptionIt(localizedContent.descriptions().get("it")); + category.setDescriptionEn(localizedContent.descriptions().get("en")); + category.setDescriptionDe(localizedContent.descriptions().get("de")); + category.setDescriptionFr(localizedContent.descriptions().get("fr")); + category.setSeoTitle(localizedContent.defaultSeoTitle()); + category.setSeoTitleIt(localizedContent.seoTitles().get("it")); + category.setSeoTitleEn(localizedContent.seoTitles().get("en")); + category.setSeoTitleDe(localizedContent.seoTitles().get("de")); + category.setSeoTitleFr(localizedContent.seoTitles().get("fr")); + category.setSeoDescription(localizedContent.defaultSeoDescription()); + category.setSeoDescriptionIt(localizedContent.seoDescriptions().get("it")); + category.setSeoDescriptionEn(localizedContent.seoDescriptions().get("en")); + category.setSeoDescriptionDe(localizedContent.seoDescriptions().get("de")); + category.setSeoDescriptionFr(localizedContent.seoDescriptions().get("fr")); category.setOgTitle(normalizeOptional(payload.getOgTitle())); category.setOgDescription(normalizeOptional(payload.getOgDescription())); category.setIndexable(payload.getIndexable() == null || payload.getIndexable()); @@ -161,14 +177,6 @@ public class AdminShopCategoryControllerService { } } - private String normalizeRequiredName(String name) { - String normalized = normalizeOptional(name); - if (normalized == null) { - throw new ResponseStatusException(BAD_REQUEST, "Category name is required"); - } - return normalized; - } - private String normalizeAndValidateSlug(String slug, String fallbackName) { String source = normalizeOptional(slug); if (source == null) { @@ -203,6 +211,103 @@ public class AdminShopCategoryControllerService { return normalized.isBlank() ? null : normalized; } + private String normalizeRequired(String value, String message) { + String normalized = normalizeOptional(value); + if (normalized == null) { + throw new ResponseStatusException(BAD_REQUEST, message); + } + return normalized; + } + + private LocalizedCategoryContent normalizeLocalizedCategoryContent(AdminUpsertShopCategoryRequest payload) { + String legacyName = normalizeOptional(payload.getName()); + String fallbackName = firstNonBlank( + legacyName, + normalizeOptional(payload.getNameIt()), + normalizeOptional(payload.getNameEn()), + normalizeOptional(payload.getNameDe()), + normalizeOptional(payload.getNameFr()) + ); + if (fallbackName == null) { + throw new ResponseStatusException(BAD_REQUEST, "Category name is required"); + } + + Map names = new LinkedHashMap<>(); + names.put("it", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameIt()), fallbackName), "Italian category name is required")); + names.put("en", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameEn()), fallbackName), "English category name is required")); + names.put("de", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameDe()), fallbackName), "German category name is required")); + names.put("fr", normalizeRequired(firstNonBlank(normalizeOptional(payload.getNameFr()), fallbackName), "French category name is required")); + + String fallbackDescription = firstNonBlank( + normalizeOptional(payload.getDescription()), + normalizeOptional(payload.getDescriptionIt()), + normalizeOptional(payload.getDescriptionEn()), + normalizeOptional(payload.getDescriptionDe()), + normalizeOptional(payload.getDescriptionFr()) + ); + Map descriptions = new LinkedHashMap<>(); + descriptions.put("it", firstNonBlank(normalizeOptional(payload.getDescriptionIt()), fallbackDescription)); + descriptions.put("en", firstNonBlank(normalizeOptional(payload.getDescriptionEn()), fallbackDescription)); + descriptions.put("de", firstNonBlank(normalizeOptional(payload.getDescriptionDe()), fallbackDescription)); + descriptions.put("fr", firstNonBlank(normalizeOptional(payload.getDescriptionFr()), fallbackDescription)); + + String fallbackSeoTitle = firstNonBlank( + normalizeOptional(payload.getSeoTitle()), + normalizeOptional(payload.getSeoTitleIt()), + normalizeOptional(payload.getSeoTitleEn()), + normalizeOptional(payload.getSeoTitleDe()), + normalizeOptional(payload.getSeoTitleFr()) + ); + Map seoTitles = new LinkedHashMap<>(); + seoTitles.put("it", firstNonBlank(normalizeOptional(payload.getSeoTitleIt()), fallbackSeoTitle)); + seoTitles.put("en", firstNonBlank(normalizeOptional(payload.getSeoTitleEn()), fallbackSeoTitle)); + seoTitles.put("de", firstNonBlank(normalizeOptional(payload.getSeoTitleDe()), fallbackSeoTitle)); + seoTitles.put("fr", firstNonBlank(normalizeOptional(payload.getSeoTitleFr()), fallbackSeoTitle)); + + String fallbackSeoDescription = firstNonBlank( + normalizeOptional(payload.getSeoDescription()), + normalizeOptional(payload.getSeoDescriptionIt()), + normalizeOptional(payload.getSeoDescriptionEn()), + normalizeOptional(payload.getSeoDescriptionDe()), + normalizeOptional(payload.getSeoDescriptionFr()) + ); + Map seoDescriptions = new LinkedHashMap<>(); + seoDescriptions.put("it", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionIt()), fallbackSeoDescription), "Italian")); + seoDescriptions.put("en", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionEn()), fallbackSeoDescription), "English")); + seoDescriptions.put("de", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionDe()), fallbackSeoDescription), "German")); + seoDescriptions.put("fr", validateSeoDescriptionLength(firstNonBlank(normalizeOptional(payload.getSeoDescriptionFr()), fallbackSeoDescription), "French")); + + return new LocalizedCategoryContent( + names.get("it"), + firstNonBlank(descriptions.get("it"), fallbackDescription), + firstNonBlank(seoTitles.get("it"), fallbackSeoTitle), + firstNonBlank(seoDescriptions.get("it"), fallbackSeoDescription), + names, + descriptions, + seoTitles, + seoDescriptions + ); + } + + private String validateSeoDescriptionLength(String value, String languageLabel) { + if (value != null && value.length() > 160) { + throw new ResponseStatusException(BAD_REQUEST, languageLabel + " SEO description must be at most 160 characters"); + } + return value; + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value; + } + } + return null; + } + private CategoryContext buildContext() { List categories = shopCategoryRepository.findAllByOrderBySortOrderAscNameAsc(); List products = shopProductRepository.findAll(); @@ -278,9 +383,25 @@ public class AdminShopCategoryControllerService { dto.setParentCategoryName(category.getParentCategory() != null ? category.getParentCategory().getName() : null); dto.setSlug(category.getSlug()); dto.setName(category.getName()); + dto.setNameIt(category.getNameIt()); + dto.setNameEn(category.getNameEn()); + dto.setNameDe(category.getNameDe()); + dto.setNameFr(category.getNameFr()); dto.setDescription(category.getDescription()); + dto.setDescriptionIt(category.getDescriptionIt()); + dto.setDescriptionEn(category.getDescriptionEn()); + dto.setDescriptionDe(category.getDescriptionDe()); + dto.setDescriptionFr(category.getDescriptionFr()); dto.setSeoTitle(category.getSeoTitle()); + dto.setSeoTitleIt(category.getSeoTitleIt()); + dto.setSeoTitleEn(category.getSeoTitleEn()); + dto.setSeoTitleDe(category.getSeoTitleDe()); + dto.setSeoTitleFr(category.getSeoTitleFr()); dto.setSeoDescription(category.getSeoDescription()); + dto.setSeoDescriptionIt(category.getSeoDescriptionIt()); + dto.setSeoDescriptionEn(category.getSeoDescriptionEn()); + dto.setSeoDescriptionDe(category.getSeoDescriptionDe()); + dto.setSeoDescriptionFr(category.getSeoDescriptionFr()); dto.setOgTitle(category.getOgTitle()); dto.setOgDescription(category.getOgDescription()); dto.setIndexable(category.getIndexable()); @@ -331,4 +452,16 @@ public class AdminShopCategoryControllerService { Map descendantProductCounts ) { } + + private record LocalizedCategoryContent( + String defaultName, + String defaultDescription, + String defaultSeoTitle, + String defaultSeoDescription, + Map names, + Map descriptions, + Map seoTitles, + Map seoDescriptions + ) { + } } diff --git a/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java b/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java index f562a92..225d952 100644 --- a/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/admin/AdminShopProductControllerService.java @@ -353,6 +353,13 @@ public class AdminShopProductControllerService { String normalizedColorName = normalizeRequired(payload.getColorName(), "Variant colorName is required"); String normalizedVariantLabel = normalizeOptional(payload.getVariantLabel()); String normalizedSku = normalizeOptional(payload.getSku()); + String fallbackColorLabel = firstNonBlank( + normalizeOptional(payload.getColorLabelIt()), + normalizeOptional(payload.getColorLabelEn()), + normalizeOptional(payload.getColorLabelDe()), + normalizeOptional(payload.getColorLabelFr()), + normalizedColorName + ); String normalizedMaterialCode = normalizeRequired( payload.getInternalMaterialCode(), "Variant internalMaterialCode is required" @@ -380,6 +387,10 @@ public class AdminShopProductControllerService { variant.setSku(normalizedSku); variant.setVariantLabel(normalizedVariantLabel != null ? normalizedVariantLabel : normalizedColorName); variant.setColorName(normalizedColorName); + variant.setColorLabelIt(firstNonBlank(normalizeOptional(payload.getColorLabelIt()), fallbackColorLabel)); + variant.setColorLabelEn(firstNonBlank(normalizeOptional(payload.getColorLabelEn()), fallbackColorLabel)); + variant.setColorLabelDe(firstNonBlank(normalizeOptional(payload.getColorLabelDe()), fallbackColorLabel)); + variant.setColorLabelFr(firstNonBlank(normalizeOptional(payload.getColorLabelFr()), fallbackColorLabel)); variant.setColorHex(normalizeColorHex(payload.getColorHex())); variant.setInternalMaterialCode(normalizedMaterialCode); variant.setPriceChf(price); @@ -531,6 +542,10 @@ public class AdminShopProductControllerService { dto.setSku(variant.getSku()); dto.setVariantLabel(variant.getVariantLabel()); dto.setColorName(variant.getColorName()); + dto.setColorLabelIt(variant.getColorLabelIt()); + dto.setColorLabelEn(variant.getColorLabelEn()); + dto.setColorLabelDe(variant.getColorLabelDe()); + dto.setColorLabelFr(variant.getColorLabelFr()); dto.setColorHex(variant.getColorHex()); dto.setInternalMaterialCode(variant.getInternalMaterialCode()); dto.setPriceChf(variant.getPriceChf()); diff --git a/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java b/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java index 164ac74..2e15aa8 100644 --- a/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/order/AdminOrderControllerService.java @@ -280,11 +280,19 @@ public class AdminOrderControllerService { itemDto.setShopProductName(item.getShopProductName()); itemDto.setShopVariantLabel(item.getShopVariantLabel()); itemDto.setShopVariantColorName(item.getShopVariantColorName()); + itemDto.setShopVariantColorLabelIt(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null); + itemDto.setShopVariantColorLabelEn(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null); + itemDto.setShopVariantColorLabelDe(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null); + itemDto.setShopVariantColorLabelFr(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null); itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); if (item.getFilamentVariant() != null) { itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); itemDto.setFilamentColorName(item.getFilamentVariant().getColorName()); + itemDto.setFilamentColorLabelIt(item.getFilamentVariant().getColorLabelIt()); + itemDto.setFilamentColorLabelEn(item.getFilamentVariant().getColorLabelEn()); + itemDto.setFilamentColorLabelDe(item.getFilamentVariant().getColorLabelDe()); + itemDto.setFilamentColorLabelFr(item.getFilamentVariant().getColorLabelFr()); itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex()); } itemDto.setQuality(item.getQuality()); diff --git a/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java b/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java index 9b1ae40..69c36cb 100644 --- a/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java +++ b/backend/src/main/java/com/printcalculator/service/order/OrderControllerService.java @@ -334,11 +334,19 @@ public class OrderControllerService { itemDto.setShopProductName(item.getShopProductName()); itemDto.setShopVariantLabel(item.getShopVariantLabel()); itemDto.setShopVariantColorName(item.getShopVariantColorName()); + itemDto.setShopVariantColorLabelIt(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null); + itemDto.setShopVariantColorLabelEn(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null); + itemDto.setShopVariantColorLabelDe(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null); + itemDto.setShopVariantColorLabelFr(item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null); itemDto.setShopVariantColorHex(item.getShopVariantColorHex()); if (item.getFilamentVariant() != null) { itemDto.setFilamentVariantId(item.getFilamentVariant().getId()); itemDto.setFilamentVariantDisplayName(item.getFilamentVariant().getVariantDisplayName()); itemDto.setFilamentColorName(item.getFilamentVariant().getColorName()); + itemDto.setFilamentColorLabelIt(item.getFilamentVariant().getColorLabelIt()); + itemDto.setFilamentColorLabelEn(item.getFilamentVariant().getColorLabelEn()); + itemDto.setFilamentColorLabelDe(item.getFilamentVariant().getColorLabelDe()); + itemDto.setFilamentColorLabelFr(item.getFilamentVariant().getColorLabelFr()); itemDto.setFilamentColorHex(item.getFilamentVariant().getColorHex()); } itemDto.setQuality(item.getQuality()); diff --git a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java index 555ecc5..375d7fa 100644 --- a/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java +++ b/backend/src/main/java/com/printcalculator/service/quote/QuoteSessionResponseAssembler.java @@ -81,7 +81,15 @@ public class QuoteSessionResponseAssembler { dto.put("shopProductName", item.getShopProductName()); dto.put("shopVariantLabel", item.getShopVariantLabel()); dto.put("shopVariantColorName", item.getShopVariantColorName()); + dto.put("shopVariantColorLabelIt", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelIt() : null); + dto.put("shopVariantColorLabelEn", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelEn() : null); + dto.put("shopVariantColorLabelDe", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelDe() : null); + dto.put("shopVariantColorLabelFr", item.getShopProductVariant() != null ? item.getShopProductVariant().getColorLabelFr() : null); dto.put("shopVariantColorHex", item.getShopVariantColorHex()); + dto.put("filamentColorLabelIt", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelIt() : null); + dto.put("filamentColorLabelEn", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelEn() : null); + dto.put("filamentColorLabelDe", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelDe() : null); + dto.put("filamentColorLabelFr", item.getFilamentVariant() != null ? item.getFilamentVariant().getColorLabelFr() : null); dto.put("materialCode", item.getMaterialCode()); dto.put("quality", item.getQuality()); dto.put("nozzleDiameterMm", item.getNozzleDiameterMm()); diff --git a/backend/src/main/java/com/printcalculator/service/shop/PublicShopCatalogService.java b/backend/src/main/java/com/printcalculator/service/shop/PublicShopCatalogService.java index e37c450..62636a1 100644 --- a/backend/src/main/java/com/printcalculator/service/shop/PublicShopCatalogService.java +++ b/backend/src/main/java/com/printcalculator/service/shop/PublicShopCatalogService.java @@ -71,7 +71,7 @@ public class PublicShopCatalogService { public List getCategories(String language) { CategoryContext categoryContext = loadCategoryContext(language); - return buildCategoryTree(null, categoryContext); + return buildCategoryTree(null, categoryContext, language); } public ShopCategoryDetailDto getCategory(String slug, String language) { @@ -83,7 +83,7 @@ public class PublicShopCatalogService { throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Category not found"); } - return buildCategoryDetail(category, categoryContext); + return buildCategoryDetail(category, categoryContext, language); } public ShopProductCatalogResponseDto getProductCatalog(String categorySlug, Boolean featuredOnly, String language) { @@ -114,7 +114,7 @@ public class PublicShopCatalogService { .toList(); ShopCategoryDetailDto selectedCategoryDetail = selectedCategory != null - ? buildCategoryDetail(selectedCategory, categoryContext) + ? buildCategoryDetail(selectedCategory, categoryContext, language) : null; return new ShopProductCatalogResponseDto( @@ -316,53 +316,63 @@ public class PublicShopCatalogService { return total; } - private List buildCategoryTree(UUID parentId, CategoryContext categoryContext) { + private List buildCategoryTree(UUID parentId, + CategoryContext categoryContext, + String language) { return categoryContext.childrenByParentId().getOrDefault(parentId, List.of()).stream() .map(category -> new ShopCategoryTreeDto( category.getId(), category.getParentCategory() != null ? category.getParentCategory().getId() : null, category.getSlug(), - category.getName(), - category.getDescription(), - category.getSeoTitle(), - category.getSeoDescription(), + category.getNameForLanguage(language), + category.getDescriptionForLanguage(language), + category.getSeoTitleForLanguage(language), + category.getSeoDescriptionForLanguage(language), category.getOgTitle(), category.getOgDescription(), category.getIndexable(), category.getSortOrder(), categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0), selectPrimaryMedia(categoryContext.categoryMediaBySlug().get(categoryMediaUsageKey(category))), - buildCategoryTree(category.getId(), categoryContext) + buildCategoryTree(category.getId(), categoryContext, language) )) .toList(); } - private ShopCategoryDetailDto buildCategoryDetail(ShopCategory category, CategoryContext categoryContext) { + private ShopCategoryDetailDto buildCategoryDetail(ShopCategory category, + CategoryContext categoryContext, + String language) { List images = categoryContext.categoryMediaBySlug().getOrDefault(categoryMediaUsageKey(category), List.of()); + String localizedSeoTitle = category.getSeoTitleForLanguage(language); + String localizedSeoDescription = category.getSeoDescriptionForLanguage(language); return new ShopCategoryDetailDto( category.getId(), category.getSlug(), - category.getName(), - category.getDescription(), - category.getSeoTitle(), - category.getSeoDescription(), + category.getNameForLanguage(language), + category.getDescriptionForLanguage(language), + localizedSeoTitle, + localizedSeoDescription, category.getOgTitle(), category.getOgDescription(), category.getIndexable(), category.getSortOrder(), categoryContext.descendantProductCounts().getOrDefault(category.getId(), 0), - buildCategoryBreadcrumbs(category), + buildCategoryBreadcrumbs(category, language), selectPrimaryMedia(images), images, - buildCategoryTree(category.getId(), categoryContext) + buildCategoryTree(category.getId(), categoryContext, language) ); } - private List buildCategoryBreadcrumbs(ShopCategory category) { + private List buildCategoryBreadcrumbs(ShopCategory category, String language) { List breadcrumbs = new ArrayList<>(); ShopCategory current = category; while (current != null) { - breadcrumbs.add(new ShopCategoryRefDto(current.getId(), current.getSlug(), current.getName())); + breadcrumbs.add(new ShopCategoryRefDto( + current.getId(), + current.getSlug(), + current.getNameForLanguage(language) + )); current = current.getParentCategory(); } java.util.Collections.reverse(breadcrumbs); @@ -399,11 +409,11 @@ public class PublicShopCatalogService { new ShopCategoryRefDto( entry.product().getCategory().getId(), entry.product().getCategory().getSlug(), - entry.product().getCategory().getName() + entry.product().getCategory().getNameForLanguage(language) ), resolvePriceFrom(entry.variants()), resolvePriceTo(entry.variants()), - toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor), + toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language), selectPrimaryMedia(images), toProductModelDto(entry) ); @@ -432,14 +442,14 @@ public class PublicShopCatalogService { new ShopCategoryRefDto( entry.product().getCategory().getId(), entry.product().getCategory().getSlug(), - entry.product().getCategory().getName() + entry.product().getCategory().getNameForLanguage(language) ), - buildCategoryBreadcrumbs(entry.product().getCategory()), + buildCategoryBreadcrumbs(entry.product().getCategory(), language), resolvePriceFrom(entry.variants()), resolvePriceTo(entry.variants()), - toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor), + toVariantDto(entry.defaultVariant(), entry.defaultVariant(), variantColorHexByMaterialAndColor, language), entry.variants().stream() - .map(variant -> toVariantDto(variant, entry.defaultVariant(), variantColorHexByMaterialAndColor)) + .map(variant -> toVariantDto(variant, entry.defaultVariant(), variantColorHexByMaterialAndColor, language)) .toList(), selectPrimaryMedia(images), images, @@ -449,7 +459,8 @@ public class PublicShopCatalogService { private ShopProductVariantOptionDto toVariantDto(ShopProductVariant variant, ShopProductVariant defaultVariant, - Map variantColorHexByMaterialAndColor) { + Map variantColorHexByMaterialAndColor, + String language) { if (variant == null) { return null; } @@ -463,6 +474,7 @@ public class PublicShopCatalogService { variant.getSku(), variant.getVariantLabel(), variant.getColorName(), + variant.getColorLabelForLanguage(language), colorHex, variant.getPriceChf(), defaultVariant != null && Objects.equals(defaultVariant.getId(), variant.getId()) diff --git a/backend/src/test/java/com/printcalculator/entity/ShopCategoryTest.java b/backend/src/test/java/com/printcalculator/entity/ShopCategoryTest.java new file mode 100644 index 0000000..ac987e8 --- /dev/null +++ b/backend/src/test/java/com/printcalculator/entity/ShopCategoryTest.java @@ -0,0 +1,55 @@ +package com.printcalculator.entity; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ShopCategoryTest { + + @Test + void localizedAccessorsShouldReturnLanguageSpecificValues() { + ShopCategory category = new ShopCategory(); + category.setName("Desk accessories"); + category.setNameIt("Accessori da scrivania"); + category.setNameEn("Desk accessories"); + category.setNameDe("Schreibtischzubehor"); + category.setNameFr("Accessoires de bureau"); + category.setDescription("Legacy description"); + category.setDescriptionIt("Organizer e accessori stampati per la scrivania."); + category.setDescriptionEn("Printed desk organizers and accessories."); + category.setDescriptionDe("Gedruckte Organizer und Zubehor fur den Schreibtisch."); + category.setDescriptionFr("Accessoires et organiseurs imprimes pour le bureau."); + category.setSeoTitle("Legacy SEO title"); + category.setSeoTitleIt("Accessori da scrivania stampati in 3D"); + category.setSeoTitleEn("3D printed desk accessories"); + category.setSeoTitleDe("3D-gedruckte Schreibtischaccessoires"); + category.setSeoTitleFr("Accessoires de bureau imprimes en 3D"); + category.setSeoDescription("Legacy SEO description"); + category.setSeoDescriptionIt("Accessori da scrivania personalizzati e funzionali."); + category.setSeoDescriptionEn("Functional custom desk accessories."); + category.setSeoDescriptionDe("Funktionale personalisierte Schreibtischaccessoires."); + category.setSeoDescriptionFr("Accessoires de bureau fonctionnels et personnalises."); + + assertEquals("Accessori da scrivania", category.getNameForLanguage("it")); + assertEquals("Desk accessories", category.getNameForLanguage("en")); + assertEquals("Schreibtischzubehor", category.getNameForLanguage("de")); + assertEquals("Accessoires de bureau", category.getNameForLanguage("fr")); + assertEquals("Gedruckte Organizer und Zubehor fur den Schreibtisch.", category.getDescriptionForLanguage("de")); + assertEquals("3D printed desk accessories", category.getSeoTitleForLanguage("en")); + assertEquals("Accessoires de bureau fonctionnels et personnalises.", category.getSeoDescriptionForLanguage("fr")); + } + + @Test + void localizedAccessorsShouldFallbackToLegacyValues() { + ShopCategory category = new ShopCategory(); + category.setName("Desk accessories"); + category.setDescription("Printed desk organizers and accessories."); + category.setSeoTitle("3D printed desk accessories"); + category.setSeoDescription("Functional custom desk accessories."); + + assertEquals("Desk accessories", category.getNameForLanguage("it")); + assertEquals("Printed desk organizers and accessories.", category.getDescriptionForLanguage("de")); + assertEquals("3D printed desk accessories", category.getSeoTitleForLanguage("fr-CH")); + assertEquals("Functional custom desk accessories.", category.getSeoDescriptionForLanguage("en-US")); + } +} diff --git a/db.sql b/db.sql index c3975e6..3ad02dd 100644 --- a/db.sql +++ b/db.sql @@ -44,6 +44,10 @@ create table filament_variant variant_display_name text not null, -- es: "PLA Nero Opaco BrandX" color_name text not null, -- Nero, Bianco, ecc. + color_label_it text, + color_label_en text, + color_label_de text, + color_label_fr text, color_hex text, finish_type text not null default 'GLOSSY' check (finish_type in ('GLOSSY', 'MATTE', 'MARBLE', 'SILK', 'TRANSLUCENT', 'SPECIAL')), @@ -70,6 +74,22 @@ select filament_variant_id, (stock_spools * spool_net_kg) as stock_kg from filament_variant; +alter table filament_variant + add column if not exists color_label_it text, + add column if not exists color_label_en text, + add column if not exists color_label_de text, + add column if not exists color_label_fr text; + +update filament_variant +set color_label_it = coalesce(nullif(btrim(color_label_it), ''), color_name), + color_label_en = coalesce(nullif(btrim(color_label_en), ''), color_name), + color_label_de = coalesce(nullif(btrim(color_label_de), ''), color_name), + color_label_fr = coalesce(nullif(btrim(color_label_fr), ''), color_name) +where nullif(btrim(color_label_it), '') is null + or nullif(btrim(color_label_en), '') is null + or nullif(btrim(color_label_de), '') is null + or nullif(btrim(color_label_fr), '') is null; + create table printer_machine_profile ( printer_machine_profile_id bigserial primary key, @@ -1013,9 +1033,25 @@ CREATE TABLE IF NOT EXISTS shop_category parent_category_id uuid REFERENCES shop_category (shop_category_id) ON DELETE SET NULL, slug text NOT NULL UNIQUE, name text NOT NULL, + name_it text, + name_en text, + name_de text, + name_fr text, description text, + description_it text, + description_en text, + description_de text, + description_fr text, seo_title text, + seo_title_it text, + seo_title_en text, + seo_title_de text, + seo_title_fr text, seo_description text, + seo_description_it text, + seo_description_en text, + seo_description_de text, + seo_description_fr text, og_title text, og_description text, indexable boolean NOT NULL DEFAULT true, @@ -1034,6 +1070,66 @@ CREATE INDEX IF NOT EXISTS ix_shop_category_parent_sort CREATE INDEX IF NOT EXISTS ix_shop_category_active_sort ON shop_category (is_active, sort_order, created_at DESC); +ALTER TABLE shop_category + ADD COLUMN IF NOT EXISTS name_it text, + ADD COLUMN IF NOT EXISTS name_en text, + ADD COLUMN IF NOT EXISTS name_de text, + ADD COLUMN IF NOT EXISTS name_fr text, + ADD COLUMN IF NOT EXISTS description_it text, + ADD COLUMN IF NOT EXISTS description_en text, + ADD COLUMN IF NOT EXISTS description_de text, + ADD COLUMN IF NOT EXISTS description_fr text, + ADD COLUMN IF NOT EXISTS seo_title_it text, + ADD COLUMN IF NOT EXISTS seo_title_en text, + ADD COLUMN IF NOT EXISTS seo_title_de text, + ADD COLUMN IF NOT EXISTS seo_title_fr text, + ADD COLUMN IF NOT EXISTS seo_description_it text, + ADD COLUMN IF NOT EXISTS seo_description_en text, + ADD COLUMN IF NOT EXISTS seo_description_de text, + ADD COLUMN IF NOT EXISTS seo_description_fr text; + +UPDATE shop_category +SET + name_it = COALESCE(NULLIF(btrim(name_it), ''), name), + name_en = COALESCE(NULLIF(btrim(name_en), ''), name), + name_de = COALESCE(NULLIF(btrim(name_de), ''), name), + name_fr = COALESCE(NULLIF(btrim(name_fr), ''), name), + description_it = COALESCE(NULLIF(btrim(description_it), ''), description), + description_en = COALESCE(NULLIF(btrim(description_en), ''), description), + description_de = COALESCE(NULLIF(btrim(description_de), ''), description), + description_fr = COALESCE(NULLIF(btrim(description_fr), ''), description), + seo_title_it = COALESCE(NULLIF(btrim(seo_title_it), ''), seo_title), + seo_title_en = COALESCE(NULLIF(btrim(seo_title_en), ''), seo_title), + seo_title_de = COALESCE(NULLIF(btrim(seo_title_de), ''), seo_title), + seo_title_fr = COALESCE(NULLIF(btrim(seo_title_fr), ''), seo_title), + seo_description_it = COALESCE(NULLIF(btrim(seo_description_it), ''), seo_description), + seo_description_en = COALESCE(NULLIF(btrim(seo_description_en), ''), seo_description), + seo_description_de = COALESCE(NULLIF(btrim(seo_description_de), ''), seo_description), + seo_description_fr = COALESCE(NULLIF(btrim(seo_description_fr), ''), seo_description) +WHERE + NULLIF(btrim(name_it), '') IS NULL + OR NULLIF(btrim(name_en), '') IS NULL + OR NULLIF(btrim(name_de), '') IS NULL + OR NULLIF(btrim(name_fr), '') IS NULL + OR (description IS NOT NULL AND ( + NULLIF(btrim(description_it), '') IS NULL + OR NULLIF(btrim(description_en), '') IS NULL + OR NULLIF(btrim(description_de), '') IS NULL + OR NULLIF(btrim(description_fr), '') IS NULL + )) + OR (seo_title IS NOT NULL AND ( + NULLIF(btrim(seo_title_it), '') IS NULL + OR NULLIF(btrim(seo_title_en), '') IS NULL + OR NULLIF(btrim(seo_title_de), '') IS NULL + OR NULLIF(btrim(seo_title_fr), '') IS NULL + )) + OR (seo_description IS NOT NULL AND ( + NULLIF(btrim(seo_description_it), '') IS NULL + OR NULLIF(btrim(seo_description_en), '') IS NULL + OR NULLIF(btrim(seo_description_de), '') IS NULL + OR NULLIF(btrim(seo_description_fr), '') IS NULL + )); + CREATE TABLE IF NOT EXISTS shop_product ( shop_product_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), @@ -1165,6 +1261,10 @@ CREATE TABLE IF NOT EXISTS shop_product_variant sku text UNIQUE, variant_label text NOT NULL, color_name text NOT NULL, + color_label_it text, + color_label_en text, + color_label_de text, + color_label_fr text, color_hex text, internal_material_code text NOT NULL, price_chf numeric(12, 2) NOT NULL DEFAULT 0.00 CHECK (price_chf >= 0), @@ -1181,6 +1281,22 @@ CREATE INDEX IF NOT EXISTS ix_shop_product_variant_product_active_sort CREATE INDEX IF NOT EXISTS ix_shop_product_variant_sku ON shop_product_variant (sku); +ALTER TABLE shop_product_variant + ADD COLUMN IF NOT EXISTS color_label_it text, + ADD COLUMN IF NOT EXISTS color_label_en text, + ADD COLUMN IF NOT EXISTS color_label_de text, + ADD COLUMN IF NOT EXISTS color_label_fr text; + +UPDATE shop_product_variant +SET color_label_it = COALESCE(NULLIF(btrim(color_label_it), ''), color_name), + color_label_en = COALESCE(NULLIF(btrim(color_label_en), ''), color_name), + color_label_de = COALESCE(NULLIF(btrim(color_label_de), ''), color_name), + color_label_fr = COALESCE(NULLIF(btrim(color_label_fr), ''), color_name) +WHERE NULLIF(btrim(color_label_it), '') IS NULL + OR NULLIF(btrim(color_label_en), '') IS NULL + OR NULLIF(btrim(color_label_de), '') IS NULL + OR NULLIF(btrim(color_label_fr), '') IS NULL; + CREATE TABLE IF NOT EXISTS shop_product_model_asset ( shop_product_model_asset_id uuid PRIMARY KEY DEFAULT gen_random_uuid(), diff --git a/frontend/src/app/core/constants/colors.const.ts b/frontend/src/app/core/constants/colors.const.ts index 1223848..4a0911b 100644 --- a/frontend/src/app/core/constants/colors.const.ts +++ b/frontend/src/app/core/constants/colors.const.ts @@ -40,93 +40,6 @@ export const PRODUCT_COLORS: ColorCategory[] = [ }, ]; -const COLOR_HEX_BY_TRANSLATION_KEY: Record = { - ...Object.fromEntries( - PRODUCT_COLORS.flatMap((category) => - category.colors.map((color) => [color.label, color.hex] as const), - ), - ), - 'COLOR.NAME.ORANGE': '#f5a623', - 'COLOR.NAME.GRAY': '#b7b7b7', - 'COLOR.NAME.LIGHT_GRAY': '#d8dadd', - 'COLOR.NAME.DARK_GRAY': '#4f4f4f', - 'COLOR.NAME.PURPLE': '#7b1fa2', - 'COLOR.NAME.BEIGE': '#d4c09a', - 'COLOR.NAME.SAND_BEIGE': '#d7c2a0', -}; - -const COLOR_TRANSLATION_KEY_BY_VALUE: Record = { - black: 'COLOR.NAME.BLACK', - nero: 'COLOR.NAME.BLACK', - noir: 'COLOR.NAME.BLACK', - schwarz: 'COLOR.NAME.BLACK', - white: 'COLOR.NAME.WHITE', - bianco: 'COLOR.NAME.WHITE', - blanc: 'COLOR.NAME.WHITE', - weiss: 'COLOR.NAME.WHITE', - red: 'COLOR.NAME.RED', - rosso: 'COLOR.NAME.RED', - rouge: 'COLOR.NAME.RED', - rot: 'COLOR.NAME.RED', - blue: 'COLOR.NAME.BLUE', - blu: 'COLOR.NAME.BLUE', - bleu: 'COLOR.NAME.BLUE', - blau: 'COLOR.NAME.BLUE', - green: 'COLOR.NAME.GREEN', - verde: 'COLOR.NAME.GREEN', - vert: 'COLOR.NAME.GREEN', - grun: 'COLOR.NAME.GREEN', - yellow: 'COLOR.NAME.YELLOW', - giallo: 'COLOR.NAME.YELLOW', - jaune: 'COLOR.NAME.YELLOW', - gelb: 'COLOR.NAME.YELLOW', - orange: 'COLOR.NAME.ORANGE', - arancione: 'COLOR.NAME.ORANGE', - naranja: 'COLOR.NAME.ORANGE', - gris: 'COLOR.NAME.GRAY', - gray: 'COLOR.NAME.GRAY', - grey: 'COLOR.NAME.GRAY', - grigio: 'COLOR.NAME.GRAY', - grau: 'COLOR.NAME.GRAY', - 'light gray': 'COLOR.NAME.LIGHT_GRAY', - 'light grey': 'COLOR.NAME.LIGHT_GRAY', - 'grigio chiaro': 'COLOR.NAME.LIGHT_GRAY', - 'gris clair': 'COLOR.NAME.LIGHT_GRAY', - hellgrau: 'COLOR.NAME.LIGHT_GRAY', - 'dark gray': 'COLOR.NAME.DARK_GRAY', - 'dark grey': 'COLOR.NAME.DARK_GRAY', - 'grigio scuro': 'COLOR.NAME.DARK_GRAY', - 'gris fonce': 'COLOR.NAME.DARK_GRAY', - dunkelgrau: 'COLOR.NAME.DARK_GRAY', - purple: 'COLOR.NAME.PURPLE', - violet: 'COLOR.NAME.PURPLE', - viola: 'COLOR.NAME.PURPLE', - lila: 'COLOR.NAME.PURPLE', - beige: 'COLOR.NAME.BEIGE', - 'sand beige': 'COLOR.NAME.SAND_BEIGE', - 'beige sabbia': 'COLOR.NAME.SAND_BEIGE', - 'beige sable': 'COLOR.NAME.SAND_BEIGE', - sandbeige: 'COLOR.NAME.SAND_BEIGE', - 'matte black': 'COLOR.NAME.MATTE_BLACK', - 'black matte': 'COLOR.NAME.MATTE_BLACK', - 'nero opaco': 'COLOR.NAME.MATTE_BLACK', - 'noir mat': 'COLOR.NAME.MATTE_BLACK', - 'matt schwarz': 'COLOR.NAME.MATTE_BLACK', - 'schwarz matt': 'COLOR.NAME.MATTE_BLACK', - 'matte white': 'COLOR.NAME.MATTE_WHITE', - 'white matte': 'COLOR.NAME.MATTE_WHITE', - 'bianco opaco': 'COLOR.NAME.MATTE_WHITE', - 'blanc mat': 'COLOR.NAME.MATTE_WHITE', - 'matt weiss': 'COLOR.NAME.MATTE_WHITE', - 'weiss matt': 'COLOR.NAME.MATTE_WHITE', - 'matte gray': 'COLOR.NAME.MATTE_GRAY', - 'matte grey': 'COLOR.NAME.MATTE_GRAY', - 'grigio opaco': 'COLOR.NAME.MATTE_GRAY', - 'gris mat': 'COLOR.NAME.MATTE_GRAY', - 'matt grau': 'COLOR.NAME.MATTE_GRAY', - 'grau matt': 'COLOR.NAME.MATTE_GRAY', -}; - export function normalizeColorValue(value: string | null | undefined): string { return String(value ?? '') .trim() @@ -138,30 +51,7 @@ export function normalizeColorValue(value: string | null | undefined): string { .replace(/\s+/g, ' '); } -export function getColorTranslationKey( - value: string | null | undefined, -): string | null { - const normalized = normalizeColorValue(value); - return normalized ? COLOR_TRANSLATION_KEY_BY_VALUE[normalized] ?? null : null; -} - -export function getColorLabelToken( - value: string | null | undefined, -): string | null { - const raw = String(value ?? '').trim(); - if (!raw) { - return null; - } - - return getColorTranslationKey(raw) ?? raw; -} - export function findColorHex(value: string | null | undefined): string | null { - const translationKey = getColorTranslationKey(value); - if (translationKey) { - return COLOR_HEX_BY_TRANSLATION_KEY[translationKey] ?? null; - } - const normalized = normalizeColorValue(value); if (!normalized) { return null; @@ -179,6 +69,52 @@ export function findColorHex(value: string | null | undefined): string | null { return null; } +export interface LocalizedColorLabelSet { + fallback?: string | null; + it?: string | null; + en?: string | null; + de?: string | null; + fr?: string | null; +} + +export function resolveLocalizedColorLabel( + language: string | null | undefined, + labels: LocalizedColorLabelSet, +): string | null { + const normalizedLanguage = String(language ?? '') + .trim() + .toLowerCase() + .split('-')[0]; + + const preferred = + normalizedLanguage === 'it' + ? labels.it + : normalizedLanguage === 'en' + ? labels.en + : normalizedLanguage === 'de' + ? labels.de + : normalizedLanguage === 'fr' + ? labels.fr + : null; + + return ( + firstNonBlank(preferred, labels.fallback) ?? + firstNonBlank(labels.it, labels.en, labels.de, labels.fr) + ); +} + +function firstNonBlank( + ...values: Array +): string | null { + for (const value of values) { + const normalized = String(value ?? '').trim(); + if (normalized) { + return normalized; + } + } + return null; +} + export function getColorHex(value: string): string { return findColorHex(value) ?? DEFAULT_BRAND_COLOR; } diff --git a/frontend/src/app/core/layout/navbar.component.ts b/frontend/src/app/core/layout/navbar.component.ts index 1e2f870..cbd17e1 100644 --- a/frontend/src/app/core/layout/navbar.component.ts +++ b/frontend/src/app/core/layout/navbar.component.ts @@ -17,7 +17,7 @@ import { import { finalize } from 'rxjs'; import { findColorHex, - getColorLabelToken, + resolveLocalizedColorLabel, } from '../constants/colors.const'; @Component({ @@ -147,15 +147,20 @@ export class NavbarComponent { } cartItemVariant(item: ShopCartItem): string | null { - return ( - item.shopVariantLabel || getColorLabelToken(item.shopVariantColorName) - ); + return item.shopVariantLabel || this.cartItemColor(item); } cartItemColor(item: ShopCartItem): string | null { return ( - getColorLabelToken(item.shopVariantColorName) ?? - getColorLabelToken(item.colorCode) + resolveLocalizedColorLabel(this.langService.selectedLang(), { + fallback: item.shopVariantColorName ?? item.colorCode, + it: item.shopVariantColorLabelIt, + en: item.shopVariantColorLabelEn, + de: item.shopVariantColorLabelDe, + fr: item.shopVariantColorLabelFr, + }) ?? + item.shopVariantColorName ?? + item.colorCode ); } diff --git a/frontend/src/app/features/admin/pages/admin-filament-stock.component.html b/frontend/src/app/features/admin/pages/admin-filament-stock.component.html index 98ed4dd..63a6d7e 100644 --- a/frontend/src/app/features/admin/pages/admin-filament-stock.component.html +++ b/frontend/src/app/features/admin/pages/admin-filament-stock.component.html @@ -101,6 +101,22 @@ placeholder="Nero, Bianco..." /> + + + + + + + + - -