Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
</parent>
<groupId>com</groupId>
<artifactId>WeatherViewer</artifactId>
<version>1.1.0-RELEASE</version>
<version>1.2.0</version>
<name>WeatherViewer</name>
<description>A Spring Boot application for checking weather forecasts, managing favorite locations, and tracking request metrics with built-in rate limiting.</description>
<description>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.</description>
<url/>
<licenses>
<license>
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/com/weatherviewer/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<String> 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;
}

}
5 changes: 5 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
126 changes: 126 additions & 0 deletions src/main/resources/messages.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# 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

# 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
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
125 changes: 125 additions & 0 deletions src/main/resources/messages_uk.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# 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=Щоденний прогноз

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=Ім'я
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=Наступна
6 changes: 3 additions & 3 deletions src/main/resources/templates/error.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

<th:block th:fragment="bodyContent">
<div class="container text-center mt-5">
<h1 class="display-4">Oops! Something Went Wrong</h1>
<p class="lead">We're sorry, but an unexpected error has occurred. Please try again later.</p>
<a class="btn btn-primary" th:href="@{/}">Go to Home</a>
<h1 class="display-4" th:text="#{error.title}">Oops! Something Went Wrong</h1>
<p class="lead" th:text="#{error.message}">We're sorry, but an unexpected error has occurred. Please try again later.</p>
<a class="btn btn-primary" th:href="@{/}" th:text="#{error.go-home}">Go to Home</a>
</div>
</th:block>

Expand Down
Loading