Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
Egyperces - Adatbázis alapú lokalizáció Spring alkalmazásban
A Spring beépített megoldása lehetőséget biztosít arra, hogy a lokalizációs készleteket (nyelvek szerinti lebontásban) properties fájlokban helyezzük el. Ezek a fájlok az alapértelmezések szerint a messages_<nyelvi kód>.properties
nevet kell, viseljék, illetve a nyelvi kódot nem tartozó változatot tekinti a Spring fallbacknek. Ez mindaddig tökéletesen megfelel, míg nem akarjuk dinamikusan változtathatóvá tenni a nyelvi készleteket, illetve amíg megelégszünk azzal, hogy a változtatások után a properties fájlokat újra kell tölteni (adott esetben a teljes alkalmazás újrafordításával). Szóval ez a megoldás nem feltétlenül kényelmes - ilyenkor válik szükségessé az egyedi megoldások keresése.
A Leaflet fejlesztése során úgy döntöttünk, elejét vesszük ennek a problémának és bár egyelőre nincs tervben a rendszer több nyelvre való felkészítése, az elsődleges nyelvi készlet is már egy dinamikus megoldással kerülne a rendszerbe. Ennek megfelelően készült el egy MongoDB alapú microservice, mely a nyelvi csomagok kezelését és kiszolgálását teszi lehetővé REST interface-en keresztül az arra igényt tartó alkalmazások számára. E cikkben a rendszer részleteire nem térnék ki, elég annyit tudni róla, hogy a nyelvi csomagokat név és nyelvi kód szerint tárolja, az alkalmazások pedig a név alapján igényelhetik a megfelelő csomagokat, melyeket minden elérhető nyelven visszaadja a rendszer. Kliens oldalon az egyedi org.springframework.context.suppor.AbstractMessageSource implementáció feladata a csomagok igénylése és azok betöltése az alkalmazás boot-up ideje alatt.
Rögtön meg is jegyezném itt, hogy a megoldáshoz szándékosan nem a MessageSource interface implementálását választottam, mivel az AbstractMessageSource osztály már tartalmaz olyan közös implementációs megoldásokat, melyeket az adatbázis alapú message source is hasznosítani tud mindenféle változtatás nélkül. Ennek megfelelően az implementáció sokkal kevésbé egetrengető feladat, mint az elsőre tűnhet. Az egyetlen metódus, amit implementálnia kell az egyedi message source-nak az a resolveCode, mely az adott szövegcímke kódját (a kulcsot) és a kívánt célnyelv kódját tartalmazza. Azt kell tehát elérnünk, hogy a MessageSource implementáció képes legyen az adatbázisban tárolt nyelvi készletekből a megfelelő fordítást visszaadni. Ehhez persze nem volna túl jó ötlet, ha minden egyes kulccsal megkérdeznénk a nyelvi készleteket kezelő service-t, szóval a MessageSource bean inicializációs lépéseként (és így az alkalmazás indulása alatt) egyszer elkérjük az összes elérhető fordítást, majd a memóriába töltve hatékonyan használhatjuk azokat.
public class TMSMessageSource extends AbstractMessageSource {
// ...
@PostConstruct
public void initMessageSource() {
// elkérjük az alkalmazás számára elérhető nyelvi csomagokat
// a nyelvi csomagokat kezelő servicetől
Set translationPacks = translationServiceClient
.retrievePacks(requiredPacks);
// a service válaszát "emészthetőbb" formátumra
// konvertálva hasznosítjuk a továbbiakban
translations = translationsConverter.convert(translationPacks);
}
@Override
protected MessageFormat resolveCode(String code, Locale locale) {
// ezzel csak meghatározok egy alapértelmezett nyelvet
// amíg az alkalmazás nem lesz felkészítve a lokalizációk kezelésére
Locale currentLocale = getLocaleToUse(locale);
// a createMessageFormat metódus az AbstractMessageSource
// osztályban van implementálva
return createMessageFormat(translations
.getTranslation(code, currentLocale), currentLocale);
}
private Locale getLocaleToUse(Locale currentLocale) {
return Optional.ofNullable(forcedLocale)
.orElse(currentLocale);
}
}
Mivel a fordításokért felelős service általános formátumban adja vissza a készleteket (természetesen ez implementációfüggő), a választ érdemes egy könnyebben kezelhető formátumra konvertálni. A választásom map struktúrára esett, melynek kulcsa a fordítás kulcsa, értéke pedig szintén egy map, ahol a kulcs a fordítás nyelve, az érték pedig a lefordított szöveg. A visszakeresés így meglehetősen könnyű és gyors lesz.
public class Translations {
private Map<String, Map<Locale, String>> translations;
public Translations() {
translations = new TreeMap<>();
}
// minden egyes a nyelvi csomagból felolvasott rekord hozzáad egy bejegyzést
public void addTranslation(String code, Locale locale, String translation) {
Map<Locale, String> translationsForCode = translations.get(code);
// a nem létező kulcsokat előbb létrehozzuk
// majd elhelyezzük az adott nyelvhez a fordítást
if (Objects.isNull(translationsForCode)) {
translations.put(code, createTranslationEntry(locale, translation));
} else {
// a létező kulcsokhoz a felbukkanó új fordításokat elhelyezzük
translationsForCode.put(locale, translation);
}
}
// ezt a metódust a resolveCode hívja minden esetben,
// mikor egy új fordításra van szüksége
// alábbi implementáció fallback működésképp a kulcsot
// adja vissza nem létező fordítások esetén
public String getTranslation(String code, Locale locale) {
return Optional.ofNullable(translations.get(code))
.map(translationEntry -> Optional
.ofNullable(translationEntry.get(locale))
.orElse(code))
.orElse(code);
}
// ...
private Map<Locale, String> createTranslationEntry(Locale locale, String translation) {
Map<Locale, String> translationEntry = new HashMap<>();
translationEntry.put(locale, translation);
return translationEntry;
}
}
Az implementált MessageSource azonban így még nem fog működni, és cikkem zárásaképp álljon itt a megoldás. Az adminisztrátori felületet Thymeleaf templating engine-nel szolgáljuk ki, melynek van egy érdekes “hibája”: kizárólag “messageSource” néven hajlandó elfogadni az aktív MessageSource implementációt. Mivel az egyedi implementációból létrejövő bean alapból nem “messageSource” néven fog elkészülni, át kell neveznünk azt, illetve elkerülendő a Spring Boot által alapértelmezetten inicializált MessageSource beannel való névütközést, a Primary annotációt sem árt elfelejteni.
@Primary
@Component("messageSource")
public class TMSMessageSource extends AbstractMessageSource {
// ...
}
A MessageSource teljes implementációja
Komment írásához jelentkezz be
Bejelentkezés
Még senki nem szólt hozzá ehhez a bejegyzéshez.