feat(back-end front-end): traslate alt and description images
All checks were successful
PR Checks / prettier-autofix (pull_request) Successful in 10s
PR Checks / security-sast (pull_request) Successful in 29s
PR Checks / test-backend (pull_request) Successful in 25s
PR Checks / test-frontend (pull_request) Successful in 1m4s

This commit is contained in:
2026-03-09 18:49:03 +01:00
parent 85598dee3b
commit e8ebef926e
18 changed files with 788 additions and 52 deletions

View File

@@ -24,7 +24,8 @@ public class PublicMediaController {
@GetMapping("/usages")
public ResponseEntity<List<PublicMediaUsageDto>> getUsageMedia(@RequestParam String usageType,
@RequestParam String usageKey) {
return ResponseEntity.ok(publicMediaQueryService.getUsageMedia(usageType, usageKey));
@RequestParam String usageKey,
@RequestParam(required = false) String lang) {
return ResponseEntity.ok(publicMediaQueryService.getUsageMedia(usageType, usageKey, lang));
}
}

View File

@@ -1,6 +1,7 @@
package com.printcalculator.dto;
import java.util.UUID;
import java.util.Map;
public class AdminCreateMediaUsageRequest {
private String usageType;
@@ -10,6 +11,7 @@ public class AdminCreateMediaUsageRequest {
private Integer sortOrder;
private Boolean isPrimary;
private Boolean isActive;
private Map<String, MediaTextTranslationDto> translations;
public String getUsageType() {
return usageType;
@@ -66,4 +68,12 @@ public class AdminCreateMediaUsageRequest {
public void setIsActive(Boolean active) {
isActive = active;
}
public Map<String, MediaTextTranslationDto> getTranslations() {
return translations;
}
public void setTranslations(Map<String, MediaTextTranslationDto> translations) {
this.translations = translations;
}
}

View File

@@ -1,6 +1,7 @@
package com.printcalculator.dto;
import java.time.OffsetDateTime;
import java.util.Map;
import java.util.UUID;
public class AdminMediaUsageDto {
@@ -12,6 +13,7 @@ public class AdminMediaUsageDto {
private Integer sortOrder;
private Boolean isPrimary;
private Boolean isActive;
private Map<String, MediaTextTranslationDto> translations;
private OffsetDateTime createdAt;
public UUID getId() {
@@ -78,6 +80,14 @@ public class AdminMediaUsageDto {
isActive = active;
}
public Map<String, MediaTextTranslationDto> getTranslations() {
return translations;
}
public void setTranslations(Map<String, MediaTextTranslationDto> translations) {
this.translations = translations;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}

View File

@@ -1,6 +1,7 @@
package com.printcalculator.dto;
import java.util.UUID;
import java.util.Map;
public class AdminUpdateMediaUsageRequest {
private String usageType;
@@ -10,6 +11,7 @@ public class AdminUpdateMediaUsageRequest {
private Integer sortOrder;
private Boolean isPrimary;
private Boolean isActive;
private Map<String, MediaTextTranslationDto> translations;
public String getUsageType() {
return usageType;
@@ -66,4 +68,12 @@ public class AdminUpdateMediaUsageRequest {
public void setIsActive(Boolean active) {
isActive = active;
}
public Map<String, MediaTextTranslationDto> getTranslations() {
return translations;
}
public void setTranslations(Map<String, MediaTextTranslationDto> translations) {
this.translations = translations;
}
}

View File

@@ -0,0 +1,22 @@
package com.printcalculator.dto;
public class MediaTextTranslationDto {
private String title;
private String altText;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAltText() {
return altText;
}
public void setAltText(String altText) {
this.altText = altText;
}
}

View File

@@ -53,6 +53,30 @@ public class MediaUsage {
@Column(name = "is_active", nullable = false)
private Boolean isActive;
@Column(name = "title_it", length = Integer.MAX_VALUE)
private String titleIt;
@Column(name = "title_en", length = Integer.MAX_VALUE)
private String titleEn;
@Column(name = "title_de", length = Integer.MAX_VALUE)
private String titleDe;
@Column(name = "title_fr", length = Integer.MAX_VALUE)
private String titleFr;
@Column(name = "alt_text_it", length = Integer.MAX_VALUE)
private String altTextIt;
@Column(name = "alt_text_en", length = Integer.MAX_VALUE)
private String altTextEn;
@Column(name = "alt_text_de", length = Integer.MAX_VALUE)
private String altTextDe;
@Column(name = "alt_text_fr", length = Integer.MAX_VALUE)
private String altTextFr;
@ColumnDefault("now()")
@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
@@ -121,6 +145,70 @@ public class MediaUsage {
isActive = active;
}
public String getTitleIt() {
return titleIt;
}
public void setTitleIt(String titleIt) {
this.titleIt = titleIt;
}
public String getTitleEn() {
return titleEn;
}
public void setTitleEn(String titleEn) {
this.titleEn = titleEn;
}
public String getTitleDe() {
return titleDe;
}
public void setTitleDe(String titleDe) {
this.titleDe = titleDe;
}
public String getTitleFr() {
return titleFr;
}
public void setTitleFr(String titleFr) {
this.titleFr = titleFr;
}
public String getAltTextIt() {
return altTextIt;
}
public void setAltTextIt(String altTextIt) {
this.altTextIt = altTextIt;
}
public String getAltTextEn() {
return altTextEn;
}
public void setAltTextEn(String altTextEn) {
this.altTextEn = altTextEn;
}
public String getAltTextDe() {
return altTextDe;
}
public void setAltTextDe(String altTextDe) {
this.altTextDe = altTextDe;
}
public String getAltTextFr() {
return altTextFr;
}
public void setAltTextFr(String altTextFr) {
this.altTextFr = altTextFr;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
@@ -128,4 +216,58 @@ public class MediaUsage {
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public String getTitleForLanguage(String language) {
if (language == null) {
return null;
}
return switch (language.trim().toLowerCase()) {
case "it" -> titleIt;
case "en" -> titleEn;
case "de" -> titleDe;
case "fr" -> titleFr;
default -> null;
};
}
public void setTitleForLanguage(String language, String value) {
if (language == null) {
return;
}
switch (language.trim().toLowerCase()) {
case "it" -> titleIt = value;
case "en" -> titleEn = value;
case "de" -> titleDe = value;
case "fr" -> titleFr = value;
default -> {
}
}
}
public String getAltTextForLanguage(String language) {
if (language == null) {
return null;
}
return switch (language.trim().toLowerCase()) {
case "it" -> altTextIt;
case "en" -> altTextEn;
case "de" -> altTextDe;
case "fr" -> altTextFr;
default -> null;
};
}
public void setAltTextForLanguage(String language, String value) {
if (language == null) {
return;
}
switch (language.trim().toLowerCase()) {
case "it" -> altTextIt = value;
case "en" -> altTextEn = value;
case "de" -> altTextDe = value;
case "fr" -> altTextFr = value;
default -> {
}
}
}
}

View File

@@ -4,6 +4,7 @@ import com.printcalculator.dto.AdminCreateMediaUsageRequest;
import com.printcalculator.dto.AdminMediaAssetDto;
import com.printcalculator.dto.AdminMediaUsageDto;
import com.printcalculator.dto.AdminMediaVariantDto;
import com.printcalculator.dto.MediaTextTranslationDto;
import com.printcalculator.dto.AdminUpdateMediaAssetRequest;
import com.printcalculator.dto.AdminUpdateMediaUsageRequest;
import com.printcalculator.entity.MediaAsset;
@@ -78,6 +79,7 @@ public class AdminMediaControllerService {
private static final Set<String> ALLOWED_UPLOAD_MIME_TYPES = Set.of(
"image/jpeg", "image/png", "image/webp"
);
private static final List<String> SUPPORTED_MEDIA_LANGUAGES = List.of("it", "en", "de", "fr");
private static final Map<String, String> GENERATED_FORMAT_MIME_TYPES = Map.of(
FORMAT_JPEG, "image/jpeg",
FORMAT_WEBP, "image/webp",
@@ -261,6 +263,7 @@ public class AdminMediaControllerService {
String usageType = requireUsageType(payload.getUsageType());
String usageKey = requireUsageKey(payload.getUsageKey());
boolean isPrimary = Boolean.TRUE.equals(payload.getIsPrimary());
Map<String, MediaTextTranslationDto> translations = requireTranslations(payload.getTranslations());
if (isPrimary) {
unsetPrimaryForScope(usageType, usageKey, payload.getOwnerId(), null);
@@ -275,6 +278,7 @@ public class AdminMediaControllerService {
usage.setIsPrimary(isPrimary);
usage.setIsActive(payload.getIsActive() == null || payload.getIsActive());
usage.setCreatedAt(OffsetDateTime.now());
applyTranslations(usage, translations);
MediaUsage saved = mediaUsageRepository.save(usage);
return toUsageDto(saved);
@@ -309,6 +313,9 @@ public class AdminMediaControllerService {
if (payload.getIsPrimary() != null) {
usage.setIsPrimary(payload.getIsPrimary());
}
if (payload.getTranslations() != null) {
applyTranslations(usage, requireTranslations(payload.getTranslations()));
}
if (Boolean.TRUE.equals(usage.getIsPrimary())) {
unsetPrimaryForScope(usage.getUsageType(), usage.getUsageKey(), usage.getOwnerId(), usage.getId());
@@ -525,6 +532,7 @@ public class AdminMediaControllerService {
dto.setSortOrder(usage.getSortOrder());
dto.setIsPrimary(usage.getIsPrimary());
dto.setIsActive(usage.getIsActive());
dto.setTranslations(extractTranslations(usage));
dto.setCreatedAt(usage.getCreatedAt());
return dto;
}
@@ -639,6 +647,96 @@ public class AdminMediaControllerService {
return normalized;
}
private Map<String, MediaTextTranslationDto> requireTranslations(Map<String, MediaTextTranslationDto> translations) {
if (translations == null || translations.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "translations are required.");
}
Map<String, MediaTextTranslationDto> normalized = new LinkedHashMap<>();
for (Map.Entry<String, MediaTextTranslationDto> entry : translations.entrySet()) {
String language = normalizeTranslationLanguage(entry.getKey());
if (normalized.containsKey(language)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Duplicate translation language: " + language + ".");
}
normalized.put(language, entry.getValue());
}
if (!normalized.keySet().equals(new LinkedHashSet<>(SUPPORTED_MEDIA_LANGUAGES))) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"translations must include exactly: " + String.join(", ", SUPPORTED_MEDIA_LANGUAGES) + "."
);
}
LinkedHashMap<String, MediaTextTranslationDto> result = new LinkedHashMap<>();
for (String language : SUPPORTED_MEDIA_LANGUAGES) {
MediaTextTranslationDto translation = normalized.get(language);
if (translation == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Missing translation for language " + language + ".");
}
String title = normalizeRequiredTranslationValue(translation.getTitle(), language, "title");
String altText = normalizeRequiredTranslationValue(translation.getAltText(), language, "altText");
MediaTextTranslationDto dto = new MediaTextTranslationDto();
dto.setTitle(title);
dto.setAltText(altText);
result.put(language, dto);
}
return result;
}
private String normalizeTranslationLanguage(String language) {
if (language == null || language.isBlank()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Translation language is required.");
}
String normalized = language.trim().toLowerCase(Locale.ROOT);
if (!SUPPORTED_MEDIA_LANGUAGES.contains(normalized)) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Unsupported translation language: " + normalized + "."
);
}
return normalized;
}
private String normalizeRequiredTranslationValue(String value, String language, String fieldName) {
String normalized = normalizeText(value);
if (normalized == null || normalized.isBlank()) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Translation " + fieldName + " is required for language " + language + "."
);
}
return normalized;
}
private void applyTranslations(MediaUsage usage, Map<String, MediaTextTranslationDto> translations) {
for (String language : SUPPORTED_MEDIA_LANGUAGES) {
MediaTextTranslationDto translation = translations.get(language);
usage.setTitleForLanguage(language, translation.getTitle());
usage.setAltTextForLanguage(language, translation.getAltText());
}
}
private Map<String, MediaTextTranslationDto> extractTranslations(MediaUsage usage) {
LinkedHashMap<String, MediaTextTranslationDto> translations = new LinkedHashMap<>();
String fallbackTitle = usage.getMediaAsset() != null ? usage.getMediaAsset().getTitle() : null;
String fallbackAltText = usage.getMediaAsset() != null ? usage.getMediaAsset().getAltText() : null;
for (String language : SUPPORTED_MEDIA_LANGUAGES) {
MediaTextTranslationDto dto = new MediaTextTranslationDto();
dto.setTitle(firstNonBlank(usage.getTitleForLanguage(language), fallbackTitle));
dto.setAltText(firstNonBlank(usage.getAltTextForLanguage(language), fallbackAltText));
translations.put(language, dto);
}
return translations;
}
private String firstNonBlank(String preferred, String fallback) {
return StringUtils.hasText(preferred) ? preferred : normalizeText(fallback);
}
private String normalizeText(String value) {
if (value == null) {
return null;

View File

@@ -34,7 +34,7 @@ public class MediaFfmpegService {
private final Set<String> availableEncoders;
public MediaFfmpegService(@Value("${media.ffmpeg.path:ffmpeg}") String ffmpegPath) {
this.ffmpegExecutable = sanitizeExecutable(ffmpegPath);
this.ffmpegExecutable = resolveExecutable(ffmpegPath);
this.availableEncoders = Collections.unmodifiableSet(loadAvailableEncoders());
}
@@ -147,7 +147,11 @@ public class MediaFfmpegService {
}
return parseAvailableEncoders(output);
} catch (Exception e) {
logger.warn("Unable to inspect FFmpeg encoders. Falling back to empty encoder list.", e);
logger.warn(
"Unable to inspect FFmpeg encoders for executable '{}'. Falling back to empty encoder list. {}",
ffmpegExecutable,
e.getMessage()
);
return Set.of();
}
}
@@ -186,6 +190,34 @@ public class MediaFfmpegService {
}
}
static String resolveExecutable(String configuredExecutable) {
String candidate = sanitizeExecutable(configuredExecutable);
try {
Path configuredPath = Path.of(candidate);
if (!configuredPath.isAbsolute()) {
return candidate;
}
if (Files.isExecutable(configuredPath)) {
return configuredPath.toString();
}
Path filename = configuredPath.getFileName();
String fallbackExecutable = filename == null ? null : filename.toString();
if (fallbackExecutable != null && !fallbackExecutable.isBlank()) {
logger.warn(
"Configured FFmpeg executable '{}' not found or not executable. Falling back to '{}' from PATH.",
configuredPath,
fallbackExecutable
);
return fallbackExecutable;
}
return candidate;
} catch (InvalidPathException e) {
throw new IllegalArgumentException("media.ffmpeg.path is not a valid executable path.", e);
}
}
private Path sanitizeMediaPath(Path path, String label, boolean requireExistingFile) throws IOException {
if (path == null) {
throw new IllegalArgumentException("Media " + label + " path is required.");

View File

@@ -31,6 +31,7 @@ public class PublicMediaQueryService {
private static final String FORMAT_JPEG = "JPEG";
private static final String FORMAT_WEBP = "WEBP";
private static final String FORMAT_AVIF = "AVIF";
private static final List<String> SUPPORTED_MEDIA_LANGUAGES = List.of("it", "en", "de", "fr");
private final MediaUsageRepository mediaUsageRepository;
private final MediaVariantRepository mediaVariantRepository;
@@ -44,9 +45,10 @@ public class PublicMediaQueryService {
this.mediaStorageService = mediaStorageService;
}
public List<PublicMediaUsageDto> getUsageMedia(String usageType, String usageKey) {
public List<PublicMediaUsageDto> getUsageMedia(String usageType, String usageKey, String language) {
String normalizedUsageType = normalizeUsageType(usageType);
String normalizedUsageKey = normalizeUsageKey(usageKey);
String normalizedLanguage = normalizeLanguage(language);
List<MediaUsage> usages = mediaUsageRepository
.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
@@ -80,7 +82,8 @@ public class PublicMediaQueryService {
return usages.stream()
.map(usage -> toDto(
usage,
variantsByAssetId.getOrDefault(usage.getMediaAsset().getId(), List.of())
variantsByAssetId.getOrDefault(usage.getMediaAsset().getId(), List.of()),
normalizedLanguage
))
.toList();
}
@@ -92,7 +95,7 @@ public class PublicMediaQueryService {
&& VISIBILITY_PUBLIC.equals(asset.getVisibility());
}
private PublicMediaUsageDto toDto(MediaUsage usage, List<MediaVariant> variants) {
private PublicMediaUsageDto toDto(MediaUsage usage, List<MediaVariant> variants, String language) {
Map<String, Map<String, MediaVariant>> variantsByPresetAndFormat = variants.stream()
.collect(Collectors.groupingBy(
MediaVariant::getVariantName,
@@ -101,8 +104,8 @@ public class PublicMediaQueryService {
PublicMediaUsageDto dto = new PublicMediaUsageDto();
dto.setMediaAssetId(usage.getMediaAsset().getId());
dto.setTitle(usage.getMediaAsset().getTitle());
dto.setAltText(usage.getMediaAsset().getAltText());
dto.setTitle(resolveLocalizedValue(usage.getTitleForLanguage(language), usage.getMediaAsset().getTitle()));
dto.setAltText(resolveLocalizedValue(usage.getAltTextForLanguage(language), usage.getMediaAsset().getAltText()));
dto.setUsageType(usage.getUsageType());
dto.setUsageKey(usage.getUsageKey());
dto.setSortOrder(usage.getSortOrder());
@@ -145,4 +148,23 @@ public class PublicMediaQueryService {
}
return usageKey.trim();
}
private String normalizeLanguage(String language) {
if (language == null || language.isBlank()) {
return "it";
}
String normalized = language.trim().toLowerCase(Locale.ROOT);
return SUPPORTED_MEDIA_LANGUAGES.contains(normalized) ? normalized : "it";
}
private String resolveLocalizedValue(String preferred, String fallback) {
if (preferred != null && !preferred.isBlank()) {
return preferred;
}
if (fallback != null && !fallback.isBlank()) {
return fallback.trim();
}
return null;
}
}

View File

@@ -4,6 +4,7 @@ import com.printcalculator.dto.AdminCreateMediaUsageRequest;
import com.printcalculator.dto.AdminMediaAssetDto;
import com.printcalculator.dto.AdminMediaUsageDto;
import com.printcalculator.dto.AdminUpdateMediaAssetRequest;
import com.printcalculator.dto.MediaTextTranslationDto;
import com.printcalculator.entity.MediaAsset;
import com.printcalculator.entity.MediaUsage;
import com.printcalculator.entity.MediaVariant;
@@ -325,6 +326,7 @@ class AdminMediaControllerServiceTest {
payload.setMediaAssetId(asset.getId());
payload.setSortOrder(5);
payload.setIsPrimary(true);
payload.setTranslations(buildTranslations("Landing hero", "Hero home alt"));
AdminMediaUsageDto created = service.createUsage(payload);
@@ -333,6 +335,8 @@ class AdminMediaControllerServiceTest {
assertEquals(asset.getId(), created.getMediaAssetId());
assertEquals(5, created.getSortOrder());
assertTrue(created.getIsPrimary());
assertEquals("Landing hero IT", created.getTranslations().get("it").getTitle());
assertEquals("Hero home alt EN", created.getTranslations().get("en").getAltText());
assertFalse(usages.get(existingPrimary.getId()).getIsPrimary());
AdminMediaAssetDto assetDto = service.getAsset(asset.getId());
@@ -340,6 +344,28 @@ class AdminMediaControllerServiceTest {
assertTrue(assetDto.getUsages().stream().anyMatch(usage -> usage.getId().equals(created.getId()) && usage.getIsPrimary()));
}
@Test
void createUsage_withoutAllTranslations_shouldFailValidation() {
MediaAsset asset = persistAsset(seedAsset("PUBLIC"));
AdminCreateMediaUsageRequest payload = new AdminCreateMediaUsageRequest();
payload.setUsageType("home");
payload.setUsageKey("landing");
payload.setMediaAssetId(asset.getId());
payload.setTranslations(new LinkedHashMap<>(Map.of(
"it", translation("Titolo IT", "Alt IT"),
"en", translation("Title EN", "Alt EN")
)));
ResponseStatusException ex = assertThrows(
ResponseStatusException.class,
() -> service.createUsage(payload)
);
assertEquals(HttpStatus.BAD_REQUEST, ex.getStatusCode());
assertTrue(ex.getReason().contains("translations must include exactly"));
}
@Test
void updateAsset_withPrivateVisibility_shouldMoveGeneratedFilesAndHidePublicUrls() throws Exception {
when(mediaImageInspector.inspect(any(Path.class))).thenReturn(
@@ -414,6 +440,22 @@ class AdminMediaControllerServiceTest {
return usage;
}
private Map<String, MediaTextTranslationDto> buildTranslations(String titleBase, String altBase) {
LinkedHashMap<String, MediaTextTranslationDto> translations = new LinkedHashMap<>();
translations.put("it", translation(titleBase + " IT", altBase + " IT"));
translations.put("en", translation(titleBase + " EN", altBase + " EN"));
translations.put("de", translation(titleBase + " DE", altBase + " DE"));
translations.put("fr", translation(titleBase + " FR", altBase + " FR"));
return translations;
}
private MediaTextTranslationDto translation(String title, String altText) {
MediaTextTranslationDto dto = new MediaTextTranslationDto();
dto.setTitle(title);
dto.setAltText(altText);
return dto;
}
private List<MediaVariant> variantsForAssets(Collection<UUID> assetIds) {
return variants.values().stream()
.filter(variant -> assetIds.contains(variant.getMediaAsset().getId()))

View File

@@ -25,6 +25,13 @@ class MediaFfmpegServiceTest {
assertEquals("media.ffmpeg.path contains control characters.", ex.getMessage());
}
@Test
void resolveExecutable_shouldFallbackToPathWhenAbsoluteLocationIsMissing() {
String resolved = MediaFfmpegService.resolveExecutable("/opt/homebrew/bin/ffmpeg");
assertEquals("ffmpeg", resolved);
}
@Test
void generateVariant_rejectsSourceNamesStartingWithDash() throws Exception {
MediaFfmpegService service = new MediaFfmpegService("missing-ffmpeg-binary");

View File

@@ -1,6 +1,7 @@
package com.printcalculator.service.media;
import com.printcalculator.dto.PublicMediaUsageDto;
import com.printcalculator.dto.MediaTextTranslationDto;
import com.printcalculator.entity.MediaAsset;
import com.printcalculator.entity.MediaUsage;
import com.printcalculator.entity.MediaVariant;
@@ -47,12 +48,20 @@ class PublicMediaQueryServiceTest {
@Test
void getUsageMedia_shouldReturnOnlyActiveReadyPublicUsagesOrderedBySortOrder() {
MediaAsset readyPublicAsset = buildAsset("READY", "PUBLIC", "Shop hero", "Shop alt");
MediaAsset readyPublicAsset = buildAsset("READY", "PUBLIC", "Shop hero fallback", "Shop alt fallback");
MediaAsset draftAsset = buildAsset("PROCESSING", "PUBLIC", "Draft", "Draft alt");
MediaAsset privateAsset = buildAsset("READY", "PRIVATE", "Private", "Private alt");
MediaUsage usageSecond = buildUsage(readyPublicAsset, "HOME_SECTION", "shop-gallery", 2, false, true);
MediaUsage usageFirst = buildUsage(readyPublicAsset, "HOME_SECTION", "shop-gallery", 1, true, true);
applyTranslation(usageSecond, "it", "Shop hero IT", "Shop alt IT");
applyTranslation(usageSecond, "en", "Shop hero EN", "Shop alt EN");
applyTranslation(usageSecond, "de", "Shop hero DE", "Shop alt DE");
applyTranslation(usageSecond, "fr", "Shop hero FR", "Shop alt FR");
applyTranslation(usageFirst, "it", "Shop hero IT", "Shop alt IT");
applyTranslation(usageFirst, "en", "Shop hero EN", "Shop alt EN");
applyTranslation(usageFirst, "de", "Shop hero DE", "Shop alt DE");
applyTranslation(usageFirst, "fr", "Shop hero FR", "Shop alt FR");
MediaUsage usageDraft = buildUsage(draftAsset, "HOME_SECTION", "shop-gallery", 0, false, true);
MediaUsage usagePrivate = buildUsage(privateAsset, "HOME_SECTION", "shop-gallery", 3, false, true);
@@ -67,20 +76,21 @@ class PublicMediaQueryServiceTest {
buildVariant(readyPublicAsset, "hero", "JPEG", "asset/hero.jpg")
));
List<PublicMediaUsageDto> result = service.getUsageMedia("home_section", "shop-gallery");
List<PublicMediaUsageDto> result = service.getUsageMedia("home_section", "shop-gallery", "en");
assertEquals(2, result.size());
assertEquals(1, result.get(0).getSortOrder());
assertEquals(Boolean.TRUE, result.get(0).getIsPrimary());
assertEquals("Shop hero", result.get(0).getTitle());
assertEquals("Shop hero EN", result.get(0).getTitle());
assertEquals("Shop alt EN", result.get(0).getAltText());
assertEquals("https://cdn.example/media/asset/thumb.jpg", result.get(0).getThumb().getJpegUrl());
assertEquals("https://cdn.example/media/asset/thumb.webp", result.get(0).getThumb().getWebpUrl());
assertEquals("https://cdn.example/media/asset/hero.avif", result.get(0).getHero().getAvifUrl());
}
@Test
void getUsageMedia_shouldReturnNullForMissingFormatsOrPresets() {
MediaAsset asset = buildAsset("READY", "PUBLIC", "Joe portrait", null);
void getUsageMedia_shouldReturnNullForMissingFormatsOrPresetsAndFallbackToAssetMetadata() {
MediaAsset asset = buildAsset("READY", "PUBLIC", "Joe portrait", "Joe portrait fallback");
MediaUsage usage = buildUsage(asset, "ABOUT_MEMBER", "joe", 0, true, true);
when(mediaUsageRepository.findByUsageTypeAndUsageKeyAndIsActiveTrueOrderBySortOrderAscCreatedAtAsc(
@@ -89,9 +99,11 @@ class PublicMediaQueryServiceTest {
when(mediaVariantRepository.findByMediaAsset_IdIn(List.of(asset.getId())))
.thenReturn(List.of(buildVariant(asset, "card", "JPEG", "joe/card.jpg")));
List<PublicMediaUsageDto> result = service.getUsageMedia("ABOUT_MEMBER", "joe");
List<PublicMediaUsageDto> result = service.getUsageMedia("ABOUT_MEMBER", "joe", "fr");
assertEquals(1, result.size());
assertEquals("Joe portrait", result.get(0).getTitle());
assertEquals("Joe portrait fallback", result.get(0).getAltText());
assertNull(result.get(0).getThumb().getJpegUrl());
assertNull(result.get(0).getCard().getAvifUrl());
assertEquals("https://cdn.example/media/joe/card.jpg", result.get(0).getCard().getJpegUrl());
@@ -139,4 +151,12 @@ class PublicMediaQueryServiceTest {
variant.setCreatedAt(OffsetDateTime.now());
return variant;
}
private void applyTranslation(MediaUsage usage, String language, String title, String altText) {
MediaTextTranslationDto translation = new MediaTextTranslationDto();
translation.setTitle(title);
translation.setAltText(altText);
usage.setTitleForLanguage(language, translation.getTitle());
usage.setAltTextForLanguage(language, translation.getAltText());
}
}