From 31ad04f5c07b297ade3f9c4f2b3814638d35f39d Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 28 Jul 2026 09:10:44 +0300 Subject: [PATCH 1/9] feat(config): configure MessageSource UTF-8 encoding and i18n setup --- .../weatherviewer/config/WebMvcConfig.java | 30 +++++++++++++++++++ src/main/resources/application.properties | 5 ++++ 2 files changed, 35 insertions(+) create mode 100644 src/main/java/com/weatherviewer/config/WebMvcConfig.java diff --git a/src/main/java/com/weatherviewer/config/WebMvcConfig.java b/src/main/java/com/weatherviewer/config/WebMvcConfig.java new file mode 100644 index 0000000..7b9af5a --- /dev/null +++ b/src/main/java/com/weatherviewer/config/WebMvcConfig.java @@ -0,0 +1,30 @@ +package com.weatherviewer.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; + +/** + * Lets any request switch the active locale by adding a {@code lang} query + * parameter (e.g. {@code ?lang=uk}). The new locale is then persisted on + * the session by {@link AppConfig#localeResolver()}, so it applies to + * subsequent requests too, not just the one carrying the parameter. + */ +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor(); + interceptor.setParamName("lang"); + return interceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6d8610d..37012af 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,6 +17,11 @@ spring.jpa.open-in-view=false spring.mvc.hiddenmethod.filter.enabled=true spring.jpa.properties.hibernate.jdbc.time_zone=UTC +# --- Internationalization --- +spring.messages.basename=messages +spring.messages.encoding=UTF-8 +spring.messages.cache-duration=${MESSAGES_CACHE_SECONDS:0} + server.servlet.session.cookie.secure=true server.servlet.session.cookie.same-site=strict From 5f0a439c4b9d8c4fb4ec6fc656901087d2eea94a Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 28 Jul 2026 09:11:29 +0300 Subject: [PATCH 2/9] feat(i18n): add default and Ukrainian message properties bundles --- src/main/resources/messages.properties | 116 ++++++++++++++++++++++ src/main/resources/messages_uk.properties | 116 ++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 src/main/resources/messages.properties create mode 100644 src/main/resources/messages_uk.properties diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..55dbdfe --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,116 @@ +# Default (English) message bundle. + +# --- Header (header-component.html) --- +header.sign-in=Sign in +header.sign-up=Sign up +header.sign-out=Sign out +header.theme.dark-mode=Switch to dark mode +header.language=Language + +# --- Sign-in page (sign-in.html) --- +signin.title=Sign in +signin.email.label=Email +signin.email.placeholder=name@example.com +signin.password.label=Password +signin.password.placeholder=Password +signin.remember-me=Remember me +signin.forgot-password=Forgot password? +signin.submit=Sign in +signin.no-account=Don't have an account yet? - +signin.sign-up-link=Sign up +signin.resend.prompt=Didn't get a verification email? +signin.resend.button=Resend verification email + +# --- Sign-up page (sign-up.html) --- +signup.title=Sign up +signup.first-name.label=First name +signup.first-name.placeholder=John +signup.last-name.label=Last name +signup.last-name.placeholder=Doe +signup.repeat-password.label=Repeat password +signup.repeat-password.placeholder=Repeat password +signup.submit=Sign up +signup.have-account=Already have an account - +signup.sign-in-link=Sign in + +# --- Shared auth links --- +back-to-sign-in=Back to sign in + +# --- Forgot password page (forgot-password.html) --- +forgot-password.title=Forgot password +forgot-password.instructions=Enter the email address on your account and we'll send you a link to reset your password. +forgot-password.submit=Send reset link + +# --- Reset password page (reset-password.html) --- +reset-password.title=Reset password +reset-password.password.label=New password +reset-password.password.placeholder=New password +reset-password.repeat-password.label=Repeat new password +reset-password.repeat-password.placeholder=Repeat new password +reset-password.submit=Set new password + +# --- Alert toasts (fragments/alerts.html) --- +alerts.success-heading=Success +alerts.error-heading=Error +alerts.errors-heading=Errors +alerts.close=Close + +# --- Error page (error.html) --- +error.title=Oops! Something Went Wrong +error.message=We're sorry, but an unexpected error has occurred. Please try again later. +error.go-home=Go to Home + +# --- Footer (footer-component.html) --- +footer.design-elements=Design elements +footer.development=Development +footer.copyright=Copyright \u00A9 2025 Pidlisnyi Lev + +# --- Search box (search-component.html) --- +search.aria-label=Search for a location +search.placeholder=Enter the location name +search.submit=Search + +# --- Search results page (search.html) --- +search.results-title=Search results +search.no-data=No data +search.add-location=Add to tracked locations + +# --- Forecast page (forecast.html) --- +forecast.hourly-title=Hourly Forecast +forecast.daily-title=Daily Forecast + +# --- Profile page (profile.html) --- +profile.title=Account Settings +profile.first-name.placeholder=First Name +profile.last-name.placeholder=Last Name +profile.password.placeholder=New Password +profile.units.label=Units +profile.units.metric=Metric (\u00B0C, m/s) +profile.units.imperial=Imperial (\u00B0F, mph) +profile.save=Save Changes + +# --- Home / dashboard page (home.html) --- +home.current-weather=Current Weather +home.sort-by=Sort by: +home.sort.date=Date +home.sort.name-asc=Name \u2191 +home.sort.name-desc=Name \u2193 +home.sort.favorite-first=Favorites First +home.sort.favorites-only=Favorites Only +home.delete=Delete +home.add-favorite=Add to Favorites +home.remove-favorite=Remove from Favorites +home.feels-like=Feels like {0}{1} +home.min=Min {0}{1} +home.max=Max {0}{1} +home.clouds=Clouds {0}% +home.humidity=Humidity {0}% +home.pressure=Pressure {0} hPa +home.wind={0}\u00B0 {1} {2} +home.sunrise=Sunrise {0} +home.sunset=Sunset {0} +home.forecast-button=Forecast +home.no-location.title=Location Required for Weather Information +home.no-location.instructions=Please add a location to view weather. You can do it by using the search bar above. +home.pagination.previous=Previous +home.pagination.next=Next diff --git a/src/main/resources/messages_uk.properties b/src/main/resources/messages_uk.properties new file mode 100644 index 0000000..4e8c31a --- /dev/null +++ b/src/main/resources/messages_uk.properties @@ -0,0 +1,116 @@ +# Ukrainian translation. Keep keys in sync with messages.properties. + +# --- Header (header-component.html) --- +header.sign-in=Увійти +header.sign-up=Реєстрація +header.sign-out=Вийти +header.theme.dark-mode=Перемкнути на темну тему +header.language=Мова + +# --- Sign-in page (sign-in.html) --- +signin.title=Вхід +signin.email.label=Електронна пошта +signin.email.placeholder=name@example.com +signin.password.label=Пароль +signin.password.placeholder=Пароль +signin.remember-me=Запам'ятати мене +signin.forgot-password=Забули пароль? +signin.submit=Увійти +signin.no-account=Ще не маєте акаунта? - +signin.sign-up-link=Зареєструватися +signin.resend.prompt=Не отримали лист для підтвердження? +signin.resend.button=Надіслати повторно + +# --- Sign-up page (sign-up.html) --- +signup.title=Реєстрація +signup.first-name.label=Ім'я +signup.first-name.placeholder=Іван +signup.last-name.label=Прізвище +signup.last-name.placeholder=Петренко +signup.repeat-password.label=Повторіть пароль +signup.repeat-password.placeholder=Повторіть пароль +signup.submit=Зареєструватися +signup.have-account=Вже маєте акаунт - +signup.sign-in-link=Увійти + +# --- Shared auth links --- +back-to-sign-in=Повернутися до входу + +# --- Forgot password page (forgot-password.html) --- +forgot-password.title=Забули пароль +forgot-password.instructions=Введіть електронну адресу вашого акаунта, і ми надішлемо посилання для скидання пароля. +forgot-password.submit=Надіслати посилання + +# --- Reset password page (reset-password.html) --- +reset-password.title=Скидання пароля +reset-password.password.label=Новий пароль +reset-password.password.placeholder=Новий пароль +reset-password.repeat-password.label=Повторіть новий пароль +reset-password.repeat-password.placeholder=Повторіть новий пароль +reset-password.submit=Встановити новий пароль + +# --- Alert toasts (fragments/alerts.html) --- +alerts.success-heading=Успіх +alerts.error-heading=Помилка +alerts.errors-heading=Помилки +alerts.close=Закрити + +# --- Error page (error.html) --- +error.title=Ой! Щось пішло не так +error.message=На жаль, сталася непередбачена помилка. Спробуйте пізніше. +error.go-home=На головну + +# --- Footer (footer-component.html) --- +footer.design-elements=Елементи дизайну +footer.development=Розробка +footer.copyright=Copyright \u00A9 2025 Pidlisnyi Lev + +# --- Search box (search-component.html) --- +search.aria-label=Пошук місця +search.placeholder=Введіть назву місця +search.submit=Пошук + +# --- Search results page (search.html) --- +search.results-title=Результати пошуку +search.no-data=Немає даних +search.add-location=Додати до відстежуваних місць + +# --- Forecast page (forecast.html) --- +forecast.hourly-title=Погодинний прогноз +forecast.daily-title=Щоденний прогноз + +# --- Profile page (profile.html) --- +profile.title=Налаштування акаунта +profile.first-name.placeholder=Ім'я +profile.last-name.placeholder=Прізвище +profile.password.placeholder=Новий пароль +profile.units.label=Одиниці виміру +profile.units.metric=Метричні (\u00B0C, м/с) +profile.units.imperial=Імперські (\u00B0F, миль/год) +profile.save=Зберегти зміни + +# --- Home / dashboard page (home.html) --- +home.current-weather=Поточна погода +home.sort-by=Сортувати за: +home.sort.date=Датою +home.sort.name-asc=Назвою \u2191 +home.sort.name-desc=Назвою \u2193 +home.sort.favorite-first=Спочатку обрані +home.sort.favorites-only=Лише обрані +home.delete=Видалити +home.add-favorite=Додати до обраних +home.remove-favorite=Прибрати з обраних +home.feels-like=Відчувається як {0}{1} +home.min=Мін. {0}{1} +home.max=Макс. {0}{1} +home.clouds=Хмарність {0}% +home.humidity=Вологість {0}% +home.pressure=Тиск {0} гПа +home.wind={0}\u00B0 {1} {2} +home.sunrise=Схід сонця {0} +home.sunset=Захід сонця {0} +home.forecast-button=Прогноз +home.no-location.title=Потрібне місце для отримання погоди +home.no-location.instructions=Додайте місце, щоб переглянути погоду. Це можна зробити за допомогою рядка пошуку вище. +home.pagination.previous=Попередня +home.pagination.next=Наступна \ No newline at end of file From cabe59543b23d6390e4a1284f1d79a5f5e53c690 Mon Sep 17 00:00:00 2001 From: podlLev Date: Tue, 28 Jul 2026 09:12:15 +0300 Subject: [PATCH 3/9] refactor(templates): update HTML views to use localized i18n message keys --- src/main/resources/templates/error.html | 6 +-- .../resources/templates/footer-component.html | 6 +-- src/main/resources/templates/forecast.html | 4 +- .../resources/templates/forgot-password.html | 13 ++--- .../resources/templates/fragments/alerts.html | 12 ++--- .../resources/templates/header-component.html | 20 ++++++-- src/main/resources/templates/home.html | 49 ++++++++++--------- src/main/resources/templates/profile.html | 26 +++++----- .../resources/templates/reset-password.html | 16 +++--- .../resources/templates/search-component.html | 6 +-- src/main/resources/templates/search.html | 6 +-- src/main/resources/templates/sign-in.html | 29 ++++++----- src/main/resources/templates/sign-up.html | 34 ++++++++----- 13 files changed, 127 insertions(+), 100 deletions(-) diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 85403df..a8a4d2d 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -7,9 +7,9 @@
-

Oops! Something Went Wrong

-

We're sorry, but an unexpected error has occurred. Please try again later.

- Go to Home +

Oops! Something Went Wrong

+

We're sorry, but an unexpected error has occurred. Please try again later.

+ Go to Home
diff --git a/src/main/resources/templates/footer-component.html b/src/main/resources/templates/footer-component.html index d1294ad..77e6e56 100644 --- a/src/main/resources/templates/footer-component.html +++ b/src/main/resources/templates/footer-component.html @@ -2,10 +2,10 @@
-

Copyright © 2025 Pidlisnyi Lev

+

Copyright © 2025 Pidlisnyi Lev

diff --git a/src/main/resources/templates/forecast.html b/src/main/resources/templates/forecast.html index 7b291a4..f48f288 100644 --- a/src/main/resources/templates/forecast.html +++ b/src/main/resources/templates/forecast.html @@ -13,7 +13,7 @@
-

Hourly Forecast

+

Hourly Forecast

@@ -30,7 +30,7 @@

Daily Forecast

+

Daily Forecast

diff --git a/src/main/resources/templates/forgot-password.html b/src/main/resources/templates/forgot-password.html index e325bdf..6c20372 100644 --- a/src/main/resources/templates/forgot-password.html +++ b/src/main/resources/templates/forgot-password.html @@ -14,19 +14,20 @@
-

Forgot password

-

Enter the email address on your account and we'll send you a link to reset your password.

+

Forgot password

+

Enter the email address on your account and we'll send you a link to reset your password.

- - + +
- +
diff --git a/src/main/resources/templates/fragments/alerts.html b/src/main/resources/templates/fragments/alerts.html index 6ce79f8..95c64d7 100644 --- a/src/main/resources/templates/fragments/alerts.html +++ b/src/main/resources/templates/fragments/alerts.html @@ -4,8 +4,8 @@ th:id="'successToast'" th:if="${successMessage != null}">
- Success - + Success +
Success message
@@ -14,8 +14,8 @@ th:id="'errorToast'" th:if="${errorMessage != null}">
- Error - + Error +
Error message
@@ -24,8 +24,8 @@ th:id="'errorsToast'" th:if="${errorMessages != null}">
- Errors - + Errors +
    diff --git a/src/main/resources/templates/header-component.html b/src/main/resources/templates/header-component.html index b3dd6cf..635e0d3 100644 --- a/src/main/resources/templates/header-component.html +++ b/src/main/resources/templates/header-component.html @@ -11,13 +11,25 @@

    Weather Viewer

- + +
+
diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 63695ad..659e337 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -14,16 +14,16 @@
-

Current Weather

+

Current Weather

- +
@@ -75,19 +75,19 @@

Location

- +
- +
- +
@@ -103,7 +103,7 @@

Location

-

+

@@ -117,15 +117,15 @@

  • - +
  • - +
  • - +
  • - +
  • @@ -152,15 +152,15 @@

  • - +
  • - +
  • - +
  • - +
  • @@ -189,7 +189,8 @@

    + th:href="@{/forecast(lat=${item.key.latitude},lon=${item.key.longitude})}" + th:text="#{home.forecast-button}"> Forecast @@ -200,15 +201,15 @@

    missing data -

    Location Required for Weather Information

    -

    Please add a location to view weather. You can do it by using the search bar above.

    +

    Location Required for Weather Information

    +

    Please add a location to view weather. You can do it by using the search bar above.

    diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html index 23edacd..5ee682e 100644 --- a/src/main/resources/templates/profile.html +++ b/src/main/resources/templates/profile.html @@ -17,42 +17,42 @@
    -

    Account Settings

    +

    Account Settings

    - - + +
    - - + +
    - - + +
    - - + +
    - +
    - +
    diff --git a/src/main/resources/templates/reset-password.html b/src/main/resources/templates/reset-password.html index 64aa10e..2967b2a 100644 --- a/src/main/resources/templates/reset-password.html +++ b/src/main/resources/templates/reset-password.html @@ -14,24 +14,26 @@
    -

    Reset password

    +

    Reset password

    - - + +
    - - + +
    - +
    diff --git a/src/main/resources/templates/search-component.html b/src/main/resources/templates/search-component.html index 04f51c0..da88ee8 100644 --- a/src/main/resources/templates/search-component.html +++ b/src/main/resources/templates/search-component.html @@ -1,8 +1,8 @@
    - - +
    diff --git a/src/main/resources/templates/search.html b/src/main/resources/templates/search.html index 0c1b5b1..ade0b5b 100644 --- a/src/main/resources/templates/search.html +++ b/src/main/resources/templates/search.html @@ -10,7 +10,7 @@
    -

    Search results

    +

    Search results

    @@ -26,13 +26,13 @@
    Name
    State - No data + No data

    - +
    diff --git a/src/main/resources/templates/sign-in.html b/src/main/resources/templates/sign-in.html index c64bd4d..87724ec 100644 --- a/src/main/resources/templates/sign-in.html +++ b/src/main/resources/templates/sign-in.html @@ -14,40 +14,43 @@
    -

    Sign in

    +

    Sign in

    - - + +
    - - + +
    - +
    - Forgot password? + Forgot password?
    - +
    -

    Don't have an account yet - - Sign up - Sign up +

    + Don't have an account yet - + Sign up + Sign up

    -

    Didn't get a verification email?

    +

    Didn't get a verification email?

    - +
    diff --git a/src/main/resources/templates/sign-up.html b/src/main/resources/templates/sign-up.html index e149a90..c50f519 100644 --- a/src/main/resources/templates/sign-up.html +++ b/src/main/resources/templates/sign-up.html @@ -12,39 +12,47 @@
    -

    Sign up

    +

    Sign up

    - - + +
    - - + +
    - - + +
    - - + +
    - - + +
    - +
    -

    Already have an account - Sign in

    +

    + Already have an account - + Sign in +

    From 4856345f7dac7de433febd8cd0b52fad197b87f6 Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 08:53:32 +0300 Subject: [PATCH 4/9] feat(weather-api): add WeatherApiLocale to resolve request locale to an OWM lang code --- .../service/integration/WeatherApiLocale.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/main/java/com/weatherviewer/service/integration/WeatherApiLocale.java diff --git a/src/main/java/com/weatherviewer/service/integration/WeatherApiLocale.java b/src/main/java/com/weatherviewer/service/integration/WeatherApiLocale.java new file mode 100644 index 0000000..a89ab74 --- /dev/null +++ b/src/main/java/com/weatherviewer/service/integration/WeatherApiLocale.java @@ -0,0 +1,42 @@ +package com.weatherviewer.service.integration; + +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Set; + +/** + * Maps the current request's locale to a language code OpenWeatherMap + * accepts for its {@code lang} query parameter, controlling the language of + * weather {@code description} text in the API response. + *

    + * Used by both {@link WeatherApiClient} (to build the actual request) and + * {@link WeatherApiCache} (to key cached responses per resolved language — + * without that, a location's weather cached for one language would be + * served as-is to a request in another language, since the raw JSON, + * descriptions included, is what's cached). + */ +public final class WeatherApiLocale { + + /** + * Languages we ask OpenWeatherMap to translate descriptions into. + * Keep in sync with the {@code messages_XX.properties} bundles this + * application ships — no point requesting a language the UI itself + * can't render around. + */ + private static final Set SUPPORTED_LANGUAGES = Set.of("en", "uk"); + + private static final String DEFAULT_LANGUAGE = "en"; + + private WeatherApiLocale() { + } + + /** + * @return the current {@link LocaleContextHolder} locale's language, + * if OpenWeatherMap is known to support it, otherwise {@code "en"}. + */ + public static String resolve() { + String language = LocaleContextHolder.getLocale().getLanguage(); + return SUPPORTED_LANGUAGES.contains(language) ? language : DEFAULT_LANGUAGE; + } + +} From b148270a190903c898e5f4322e3f52230d2e8b8c Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 08:53:49 +0300 Subject: [PATCH 5/9] feat(weather-api): request OpenWeatherMap descriptions in the resolved locale --- .../service/integration/WeatherApiClient.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/weatherviewer/service/integration/WeatherApiClient.java b/src/main/java/com/weatherviewer/service/integration/WeatherApiClient.java index 64c297b..534bc6c 100644 --- a/src/main/java/com/weatherviewer/service/integration/WeatherApiClient.java +++ b/src/main/java/com/weatherviewer/service/integration/WeatherApiClient.java @@ -178,24 +178,24 @@ private static String maskApiKey(String url) { return APPID_PATTERN.matcher(url).replaceAll("$1***"); } - /** Builds a request URL for a city-name-based endpoint, with metric units and English descriptions. */ + /** Builds a request URL for a city-name-based endpoint, with metric units and locale-appropriate descriptions. */ private String buildCityUrl(String suffix, String city) { return UriComponentsBuilder.fromUri(URI.create(baseApiUrl + suffix)) .queryParam("appid", apiKey) .queryParam("units", "metric") - .queryParam("lang", "en") + .queryParam("lang", WeatherApiLocale.resolve()) .queryParam("q", city) .encode(StandardCharsets.UTF_8) .build() .toUriString(); } - /** Builds a request URL for a coordinate-based endpoint, with metric units and English descriptions. */ + /** Builds a request URL for a coordinate-based endpoint, with metric units and locale-appropriate descriptions. */ private String buildCoordsUrl(String suffix, double latitude, double longitude) { return UriComponentsBuilder.fromUri(URI.create(baseApiUrl + suffix)) .queryParam("appid", apiKey) .queryParam("units", "metric") - .queryParam("lang", "en") + .queryParam("lang", WeatherApiLocale.resolve()) .queryParam("lat", latitude) .queryParam("lon", longitude) .encode(StandardCharsets.UTF_8) From 1a8e236b9f3eeef7d963dff34ea11e0013fc6a3f Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 08:54:42 +0300 Subject: [PATCH 6/9] test(weather-api): pin LocaleContextHolder in WeatherApiClientTest, add uk/fallback cases --- .../integration/WeatherApiClientTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/test/java/com/weatherviewer/service/integration/WeatherApiClientTest.java b/src/test/java/com/weatherviewer/service/integration/WeatherApiClientTest.java index 5eb62e7..0e77d2c 100644 --- a/src/test/java/com/weatherviewer/service/integration/WeatherApiClientTest.java +++ b/src/test/java/com/weatherviewer/service/integration/WeatherApiClientTest.java @@ -16,6 +16,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.slf4j.LoggerFactory; +import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClientResponseException; @@ -23,6 +24,7 @@ import java.lang.reflect.Method; import java.net.URI; import java.util.List; +import java.util.Locale; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -62,6 +64,10 @@ void setUp() { ReflectionTestUtils.setField(client, "forecastApiUrlSuffix", "/data/2.5/forecast"); ReflectionTestUtils.setField(client, "geocodingApiUrlSuffix", "/geo/1.0/direct"); + // WeatherApiLocale.resolve() reads LocaleContextHolder; pin it so the + // "lang=en" assertions below don't depend on the host JVM's default locale. + LocaleContextHolder.setLocale(Locale.ENGLISH); + lenient().doReturn(requestHeadersUriSpec).when(restClient).get(); lenient().doReturn(requestHeadersSpec).when(requestHeadersUriSpec).uri(any(URI.class)); lenient().when(requestHeadersSpec.retrieve()).thenReturn(responseSpec); @@ -74,6 +80,7 @@ void setUp() { @AfterEach void tearDown() { ((Logger) LoggerFactory.getLogger(WeatherApiClient.class)).detachAppender(logAppender); + LocaleContextHolder.resetLocaleContext(); } @Test @@ -242,6 +249,34 @@ void fetchCurrentWeatherByCity_buildsUrlWithMetricUnitsAndEnglishLang() throws E assertThat(uri).contains("appid=" + SECRET_API_KEY); } + @Test + void fetchCurrentWeatherByCity_ukrainianLocale_buildsUrlWithUkrainianLang() throws Exception { + LocaleContextHolder.setLocale(new Locale("uk")); + JsonNode mockNode = mock(JsonNode.class); + when(responseSpec.body(String.class)).thenReturn("{}"); + when(objectMapper.readTree(anyString())).thenReturn(mockNode); + + client.fetchCurrentWeatherByCity("Kyiv"); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + verify(requestHeadersUriSpec).uri(uriCaptor.capture()); + assertThat(uriCaptor.getValue().toString()).contains("lang=uk"); + } + + @Test + void fetchCurrentWeatherByCity_unsupportedLocale_fallsBackToEnglishLang() throws Exception { + LocaleContextHolder.setLocale(Locale.GERMAN); + JsonNode mockNode = mock(JsonNode.class); + when(responseSpec.body(String.class)).thenReturn("{}"); + when(objectMapper.readTree(anyString())).thenReturn(mockNode); + + client.fetchCurrentWeatherByCity("Kyiv"); + + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + verify(requestHeadersUriSpec).uri(uriCaptor.capture()); + assertThat(uriCaptor.getValue().toString()).contains("lang=en"); + } + @Test void fetchForecastByCity_buildsUrlWithForecastSuffix() throws Exception { JsonNode mockNode = mock(JsonNode.class); From 3ce61c738fca1ef8b112161fb4533613157d79a9 Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 08:54:53 +0300 Subject: [PATCH 7/9] test(weather-api): add WeatherApiLocaleTest --- .../integration/WeatherApiLocaleTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/test/java/com/weatherviewer/service/integration/WeatherApiLocaleTest.java diff --git a/src/test/java/com/weatherviewer/service/integration/WeatherApiLocaleTest.java b/src/test/java/com/weatherviewer/service/integration/WeatherApiLocaleTest.java new file mode 100644 index 0000000..00e3ba4 --- /dev/null +++ b/src/test/java/com/weatherviewer/service/integration/WeatherApiLocaleTest.java @@ -0,0 +1,42 @@ +package com.weatherviewer.service.integration; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; + +class WeatherApiLocaleTest { + + @AfterEach + void tearDown() { + LocaleContextHolder.resetLocaleContext(); + } + + @Test + void resolve_english_returnsEn() { + LocaleContextHolder.setLocale(Locale.ENGLISH); + assertThat(WeatherApiLocale.resolve()).isEqualTo("en"); + } + + @Test + void resolve_ukrainian_returnsUk() { + LocaleContextHolder.setLocale(new Locale("uk")); + assertThat(WeatherApiLocale.resolve()).isEqualTo("uk"); + } + + @Test + void resolve_ukrainianRegionVariant_returnsUk() { + LocaleContextHolder.setLocale(new Locale("uk", "UA")); + assertThat(WeatherApiLocale.resolve()).isEqualTo("uk"); + } + + @Test + void resolve_unsupportedLanguage_fallsBackToEn() { + LocaleContextHolder.setLocale(Locale.GERMAN); + assertThat(WeatherApiLocale.resolve()).isEqualTo("en"); + } + +} From 52b214ca7e180a9bb0b3db23f05fb1b4134751fc Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 08:55:51 +0300 Subject: [PATCH 8/9] feat(i18n): add weather-condition.* translated labels for the WeatherCondition enum --- src/main/resources/messages.properties | 12 +++++++++++- src/main/resources/messages_uk.properties | 9 +++++++++ src/main/resources/templates/forecast.html | 4 ++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index 55dbdfe..75e2a6b 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -79,6 +79,16 @@ search.add-location=Add to tracked locations forecast.hourly-title=Hourly Forecast forecast.daily-title=Daily Forecast +# Fallback labels for both forecast sections when a real per-reading +weather-condition.THUNDERSTORM=Thunderstorm +weather-condition.DRIZZLE=Drizzle +weather-condition.RAIN=Rain +weather-condition.SNOW=Snow +weather-condition.ATMOSPHERE=Fog/Mist +weather-condition.CLEAR=Clear +weather-condition.CLOUDS=Clouds +weather-condition.UNDEFINED=No data + # --- Profile page (profile.html) --- profile.title=Account Settings profile.first-name.placeholder=First Name @@ -113,4 +123,4 @@ home.forecast-button=Forecast home.no-location.title=Location Required for Weather Information home.no-location.instructions=Please add a location to view weather. You can do it by using the search bar above. home.pagination.previous=Previous -home.pagination.next=Next +home.pagination.next=Next \ No newline at end of file diff --git a/src/main/resources/messages_uk.properties b/src/main/resources/messages_uk.properties index 4e8c31a..d6836ba 100644 --- a/src/main/resources/messages_uk.properties +++ b/src/main/resources/messages_uk.properties @@ -79,6 +79,15 @@ search.add-location=Додати до відстежуваних місць forecast.hourly-title=Погодинний прогноз forecast.daily-title=Щоденний прогноз +weather-condition.THUNDERSTORM=Гроза +weather-condition.DRIZZLE=Мряка +weather-condition.RAIN=Дощ +weather-condition.SNOW=Сніг +weather-condition.ATMOSPHERE=Туман +weather-condition.CLEAR=Ясно +weather-condition.CLOUDS=Хмарно +weather-condition.UNDEFINED=Немає даних + # --- Profile page (profile.html) --- profile.title=Налаштування акаунта profile.first-name.placeholder=Ім'я diff --git a/src/main/resources/templates/forecast.html b/src/main/resources/templates/forecast.html index f48f288..b03839f 100644 --- a/src/main/resources/templates/forecast.html +++ b/src/main/resources/templates/forecast.html @@ -22,7 +22,7 @@

    Hourly Forecast

    -

    +

    @@ -43,7 +43,7 @@

    -

    +

    From ec2ec247a1de84d802914a9ec695430cd964ca14 Mon Sep 17 00:00:00 2001 From: podlLev Date: Wed, 29 Jul 2026 09:25:42 +0300 Subject: [PATCH 9/9] chore(release): bump version to 1.2.0 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8d50792..2928ac7 100644 --- a/pom.xml +++ b/pom.xml @@ -10,9 +10,9 @@ com WeatherViewer - 1.1.0-RELEASE + 1.2.0 WeatherViewer - A Spring Boot application for checking weather forecasts, managing favorite locations, and tracking request metrics with built-in rate limiting. + A Spring Boot application for checking weather forecasts, managing favorite locations, and tracking request metrics with built-in rate limiting. Supports English and Ukrainian locales.