Internationalization Enhancement
Some checks failed
Java CI with Maven / build (17) (push) Failing after 1s
Java CI with Gradle / build (17) (push) Failing after 7s

- Added test to ensure there are no string literals
- Added test to ensure a string is translated in all language files
- Added missing strings in properties
- Internationalized remaining strings flagged by the tests

Signed-off-by: anizmo <potdar.anuj@gmail.com>
This commit is contained in:
anizmo 2025-04-30 21:57:06 -04:00 committed by Dave Syer
parent 0c88f916db
commit c5af32d5a1
18 changed files with 313 additions and 55 deletions

View file

@ -31,3 +31,18 @@ pets=Pets
home=Home
error=Error
telephone.invalid=Telephone must be a 10-digit number
layoutTitle=PetClinic :: a Spring Framework demonstration
pet=Pet
birthDate=Birth Date
type=Type
previousVisits=Previous Visits
date=Date
description=Description
new=New
addVisit=Add Visit
editPet=Edit Pet
ownerInformation=Owner Information
visitDate=Visit Date
editOwner=Edit Owner
addNewPet=Add New Pet
petsAndVisits=Pets and Visits

View file

@ -31,3 +31,18 @@ pets=Haustiere
home=Startseite
error=Fehler
telephone.invalid=Telefonnummer muss aus 10 Ziffern bestehen
layoutTitle=PetClinic :: eine Demonstration des Spring Frameworks
pet=Haustier
birthDate=Geburtsdatum
type=Typ
previousVisits=Frühere Besuche
date=Datum
description=Beschreibung
new=Neu
addVisit=Besuch hinzufügen
editPet=Haustier bearbeiten
ownerInformation=Besitzerinformationen
visitDate=Besuchsdatum
editOwner=Besitzer bearbeiten
addNewPet=Neues Haustier hinzufügen
petsAndVisits=Haustiere und Besuche

View file

@ -31,3 +31,18 @@ pets=Mascotas
home=Inicio
error=Error
telephone.invalid=El número de teléfono debe tener 10 dígitos
layoutTitle=PetClinic :: una demostración de Spring Framework
pet=Mascota
birthDate=Fecha de nacimiento
type=Tipo
previousVisits=Visitas anteriores
date=Fecha
description=Descripción
new=Nuevo
addVisit=Agregar visita
editPet=Editar mascota
ownerInformation=Información del propietario
visitDate=Fecha de visita
editOwner=Editar propietario
addNewPet=Agregar nueva mascota
petsAndVisits=Mascotas y visitas

View file

@ -31,3 +31,18 @@ pets=حیوانات خانگی
home=خانه
error=خطا
telephone.invalid=شماره تلفن باید ۱۰ رقمی باشد
layoutTitle=PetClinic :: یک نمایش از Spring Framework
pet=حیوان خانگی
birthDate=تاریخ تولد
type=نوع
previousVisits=ویزیت‌های قبلی
date=تاریخ
description=توضیحات
new=جدید
addVisit=افزودن ویزیت
editPet=ویرایش حیوان خانگی
ownerInformation=اطلاعات مالک
visitDate=تاریخ ویزیت
editOwner=ویرایش مالک
addNewPet=افزودن حیوان خانگی جدید
petsAndVisits=حیوانات و ویزیت‌ها

View file

@ -31,3 +31,18 @@ pets=반려동물
home=
error=오류
telephone.invalid=전화번호는 10자리 숫자여야 합니다
layoutTitle=PetClinic :: Spring Framework 데모
pet=반려동물
birthDate=생년월일
type=종류
previousVisits=이전 방문
date=날짜
description=설명
new=새로운
addVisit=방문 추가
editPet=반려동물 수정
ownerInformation=소유자 정보
visitDate=방문 날짜
editOwner=소유자 수정
addNewPet=새 반려동물 추가
petsAndVisits=반려동물 및 방문

View file

@ -31,3 +31,18 @@ pets=Animais de estimação
home=Início
error=Erro
telephone.invalid=O número de telefone deve conter 10 dígitos
layoutTitle=PetClinic :: uma demonstração do Spring Framework
pet=Animal de estimação
birthDate=Data de nascimento
type=Tipo
previousVisits=Visitas anteriores
date=Data
description=Descrição
new=Novo
addVisit=Adicionar visita
editPet=Editar animal
ownerInformation=Informações do proprietário
visitDate=Data da visita
editOwner=Editar proprietário
addNewPet=Adicionar novo animal
petsAndVisits=Animais e visitas

View file

@ -31,3 +31,18 @@ pets=Питомцы
home=Главная
error=Ошибка
telephone.invalid=Телефон должен содержать 10 цифр
layoutTitle=PetClinic :: демонстрация Spring Framework
pet=Питомец
birthDate=Дата рождения
type=Тип
previousVisits=Предыдущие визиты
date=Дата
description=Описание
new=Новый
addVisit=Добавить визит
editPet=Редактировать питомца
ownerInformation=Информация о владельце
visitDate=Дата визита
editOwner=Редактировать владельца
addNewPet=Добавить нового питомца
petsAndVisits=Питомцы и визиты

View file

@ -31,3 +31,18 @@ pets=Evcil Hayvanlar
home=Ana Sayfa
error=Hata
telephone.invalid=Telefon numarası 10 basamaklı olmalıdır
layoutTitle=PetClinic :: bir Spring Framework demosu
pet=Evcil Hayvan
birthDate=Doğum Tarihi
type=Tür
previousVisits=Önceki Ziyaretler
date=Tarih
description=ıklama
new=Yeni
addVisit=Ziyaret Ekle
editPet=Evcil Hayvanı Düzenle
ownerInformation=Sahip Bilgileri
visitDate=Ziyaret Tarihi
editOwner=Sahibi Düzenle
addNewPet=Yeni Evcil Hayvan Ekle
petsAndVisits=Evcil Hayvanlar ve Ziyaretler

View file

@ -1,4 +1,4 @@
<html>
<html xmlns:th="https://www.thymeleaf.org">
<body>
<form>
<th:block th:fragment="input (label, name, type)">
@ -18,7 +18,7 @@
<span
class="fa fa-remove form-control-feedback"
aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}">Error</span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>

View file

@ -1,5 +1,6 @@
<!doctype html>
<html th:fragment="layout (template, menu)">
<html th:fragment="layout (template, menu)"
xmlns:th="https://www.thymeleaf.org">
<head>
@ -10,7 +11,7 @@
<link rel="shortcut icon" type="image/x-icon" th:href="@{/resources/images/favicon.png}">
<title>PetClinic :: a Spring Framework demonstration</title>
<title th:text="#{layoutTitle}">PetClinic :: a Spring Framework demonstration</title>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>

View file

@ -1,4 +1,4 @@
<html>
<html xmlns:th="https://www.thymeleaf.org">
<body>
<form>
<th:block th:fragment="select (label, name, items)">
@ -19,7 +19,7 @@
<span
class="fa fa-remove form-control-feedback"
aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}">Error</span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>

View file

@ -12,10 +12,12 @@
<label class="col-sm-2 control-label" th:text="#{lastName}">Last name </label>
<div class="col-sm-10">
<input class="form-control" th:field="*{lastName}" size="30"
maxlength="80" /> <span class="help-inline"><div
th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">Error</p>
</div></span>
maxlength="80" />
<span class="help-inline">
<div th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">Error</p>
</div>
</span>
</div>
</div>
</div>

View file

@ -6,7 +6,7 @@
<body>
<h2>Owner Information</h2>
<h2 th:text="#{ownerInformation}">Owner Information</h2>
<div th:if="${message}" class="alert alert-success" id="success-message">
<span th:text="${message}"></span>
@ -21,44 +21,44 @@
<table class="table table-striped" th:object="${owner}">
<tr>
<th>Name</th>
<th th:text="#{name}">Name</th>
<td><b th:text="*{firstName + ' ' + lastName}"></b></td>
</tr>
<tr>
<th>Address</th>
<th th:text="#{address}">Address</th>
<td th:text="*{address}"></td>
</tr>
<tr>
<th>City</th>
<th th:text="#{city}">City</th>
<td th:text="*{city}"></td>
</tr>
<tr>
<th>Telephone</th>
<th th:text="#{telephone}">Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
</table>
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary">Edit
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary" th:text="#{editOwner}">Edit
Owner</a>
<a th:href="@{__${owner.id}__/pets/new}" class="btn btn-primary">Add
<a th:href="@{__${owner.id}__/pets/new}" class="btn btn-primary" th:text="#{addNewPet}">Add
New Pet</a>
<br />
<br />
<br />
<h2>Pets and Visits</h2>
<h2 th:text="#{petsAndVisits}">Pets and Visits</h2>
<table class="table table-striped">
<tr th:each="pet : ${owner.pets}">
<td valign="top">
<dl class="dl-horizontal">
<dt>Name</dt>
<dt th:text="#{name}">Name</dt>
<dd th:text="${pet.name}"></dd>
<dt>Birth Date</dt>
<dt th:text="#{birthDate}">Birth Date</dt>
<dd
th:text="${#temporals.format(pet.birthDate, 'yyyy-MM-dd')}"></dd>
<dt>Type</dt>
<dt th:text="#{type}">Type</dt>
<dd th:text="${pet.type}"></dd>
</dl>
</td>
@ -66,8 +66,8 @@
<table class="table-condensed">
<thead>
<tr>
<th>Visit Date</th>
<th>Description</th>
<th th:text="#{visitDate}">Visit Date</th>
<th th:text="#{description}">Description</th>
</tr>
</thead>
<tr th:each="visit : ${pet.visits}">
@ -75,8 +75,8 @@
<td th:text="${visit?.description}"></td>
</tr>
<tr>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/edit}">Edit Pet</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/visits/new}">Add Visit</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/edit}" th:text="#{editPet}">Edit Pet</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/visits/new}" th:text="#{addVisit}">Add Visit</a></td>
</tr>
</table>
</td>

View file

@ -37,24 +37,24 @@
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" title="First"
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" th:title="#{first}"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" title="First" class="fa fa-fast-backward"></span>
<span th:unless="${currentPage > 1}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" title="Previous"
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" title="Previous" class="fa fa-step-backward"></span>
<span th:unless="${currentPage > 1}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" title="Next"
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Next" class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" title="Last"
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Last" class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-step-forward"></span>
</span>
</div>
</body>

View file

@ -4,14 +4,14 @@
<body>
<h2>
<th:block th:if="${pet['new']}">New </th:block>
Pet
<th:block th:if="${pet['new']}" th:text="#{new}">New </th:block>
<span th:text="#{pet}">Pet</span>
</h2>
<form th:object="${pet}" class="form-horizontal" method="post">
<input type="hidden" name="id" th:value="*{id}" />
<div class="form-group has-feedback">
<div class="form-group">
<label class="col-sm-2 control-label">Owner</label>
<label class="col-sm-2 control-label" th:text="#{owner}">Owner</label>
<div class="col-sm-10">
<span th:text="${owner?.firstName + ' ' + owner?.lastName}" />
</div>
@ -27,8 +27,7 @@
<div class="col-sm-offset-2 col-sm-10">
<button
th:with="text=${pet['new']} ? 'Add Pet' : 'Update Pet'"
class="btn btn-primary" type="submit" th:text="${text}">Add
Pet</button>
class="btn btn-primary" type="submit" th:text="${text}">Add Pet</button>
</div>
</div>
</form>

View file

@ -4,18 +4,18 @@
<body>
<h2>
<th:block th:if="${visit['new']}">New </th:block>
<th:block th:if="${visit['new']}" th:text="#{new}">New </th:block>
Visit
</h2>
<b>Pet</b>
<b th:text="#{pet}">Pet</b>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Birth Date</th>
<th>Type</th>
<th>Owner</th>
<th th:text="#{name}">Name</th>
<th th:text="#{birthDate}">Birth Date</th>
<th th:text="#{type}">Type</th>
<th th:text="#{owner}">Owner</th>
</tr>
</thead>
<tr>
@ -39,17 +39,17 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="hidden" name="petId" th:value="${pet.id}" />
<button class="btn btn-primary" type="submit">Add Visit</button>
<button class="btn btn-primary" type="submit" th:text="${addVisit}">Add Visit</button>
</div>
</div>
</form>
<br />
<b>Previous Visits</b>
<b th:text="#{previousVisits}">Previous Visits</b>
<table class="table table-striped">
<tr>
<th>Date</th>
<th>Description</th>
<th th:text="#{date}">Date</th>
<th th:text="#{description}">Description</th>
</tr>
<tr th:if="${!visit['new']}" th:each="visit : ${pet.visits}">
<td th:text="${#temporals.format(visit.date, 'yyyy-MM-dd')}"></td>

View file

@ -33,22 +33,22 @@
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=1'}" title=#{first}
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=1'}" th:title="#{first}"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" th:text="#{first}" title=#{first} class="fa fa-fast-backward"></span>
<span th:unless="${currentPage > 1}" th:text="#{first}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=__${currentPage - 1}__'}" title=#{previous}
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" th:text="#{previous}" title=#{previous} class="fa fa-step-backward"></span>
<span th:unless="${currentPage > 1}" th:text="#{previous}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${currentPage + 1}__'}" title=#{next}
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" th:text="#{next}" title=#{next} class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:text="#{next}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${totalPages}__'}" title=#{last}
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" th:text="#{last}" class="fa fa-fast-forward"></span>
</span>

View file

@ -0,0 +1,136 @@
package org.springframework.samples.petclinic.system;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.fail;
/**
* This test ensures that there are no hard-coded strings without internationalization in
* any HTML files. Also ensures that a string is translated in every language to avoid
* partial translations.
*
* @author Anuj Ashok Potdar
*/
public class I18nPropertiesSyncTest {
private static final String I18N_DIR = "src/main/resources";
private static final String BASE_NAME = "messages";
public static final String PROPERTIES = ".properties";
private static final Pattern HTML_TEXT_LITERAL = Pattern.compile(">([^<>{}]+)<");
private static final Pattern BRACKET_ONLY = Pattern.compile("<[^>]*>\\s*[\\[\\]](?:&nbsp;)?\\s*</[^>]*>");
private static final Pattern HAS_TH_TEXT_ATTRIBUTE = Pattern.compile("th:(u)?text\\s*=\\s*\"[^\"]+\"");
@Test
public void checkNonInternationalizedStrings() throws IOException {
Path root = Paths.get("src/main");
List<Path> files;
try (Stream<Path> stream = Files.walk(root)) {
files = stream.filter(p -> p.toString().endsWith(".java") || p.toString().endsWith(".html"))
.filter(p -> !p.toString().contains("/test/"))
.filter(p -> !p.getFileName().toString().endsWith("Test.java"))
.toList();
}
StringBuilder report = new StringBuilder();
for (Path file : files) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (line.startsWith("//") || line.startsWith("@") || line.contains("log.")
|| line.contains("System.out"))
continue;
if (file.toString().endsWith(".html")) {
boolean hasLiteralText = HTML_TEXT_LITERAL.matcher(line).find();
boolean hasThTextAttribute = HAS_TH_TEXT_ATTRIBUTE.matcher(line).find();
boolean isBracketOnly = BRACKET_ONLY.matcher(line).find();
if (hasLiteralText && !line.contains("#{") && !hasThTextAttribute && !isBracketOnly) {
report.append("HTML: ")
.append(file)
.append(" Line ")
.append(i + 1)
.append(": ")
.append(line)
.append("\n");
}
}
}
}
if (!report.isEmpty()) {
fail("Hardcoded (non-internationalized) strings found:\n" + report);
}
}
@Test
public void checkI18nPropertyFilesAreInSync() throws IOException {
List<Path> propertyFiles;
try (Stream<Path> stream = Files.walk(Paths.get(I18N_DIR))) {
propertyFiles = stream.filter(p -> p.getFileName().toString().startsWith(BASE_NAME))
.filter(p -> p.getFileName().toString().endsWith(PROPERTIES))
.toList();
}
Map<String, Properties> localeToProps = new HashMap<>();
for (Path path : propertyFiles) {
Properties props = new Properties();
try (var reader = Files.newBufferedReader(path)) {
props.load(reader);
localeToProps.put(path.getFileName().toString(), props);
}
}
String baseFile = BASE_NAME + PROPERTIES;
Properties baseProps = localeToProps.get(baseFile);
if (baseProps == null) {
fail("Base properties file '" + baseFile + "' not found.");
return;
}
Set<String> baseKeys = baseProps.stringPropertyNames();
StringBuilder report = new StringBuilder();
for (Map.Entry<String, Properties> entry : localeToProps.entrySet()) {
String fileName = entry.getKey();
// We use fallback logic to include english strings, hence messages_en is not
// populated.
if (fileName.equals(baseFile) || fileName.equals("messages_en.properties"))
continue;
Properties props = entry.getValue();
Set<String> missingKeys = new TreeSet<>(baseKeys);
missingKeys.removeAll(props.stringPropertyNames());
if (!missingKeys.isEmpty()) {
report.append("Missing keys in ").append(fileName).append(":\n");
missingKeys.forEach(k -> report.append(" ").append(k).append("\n"));
}
}
if (!report.isEmpty()) {
fail("Translation files are not in sync:\n" + report);
}
}
}