Messages and languages
Every error answer has a code a program can branch on, and its words in one language per
answer:
- The languages in
Accept-Language, in order - the first one the application has translated (amessages_<lang>, a<domain>-messages_<lang>beside the domain bundle, orValidationMessages_<lang>on the classpath), the default language, or English. - Otherwise (no header, or a language without a translation) the default language: Spring
Boot's
spring.web.locale(the same name in Boot 3 and 4); without it the bundle's default wording (English, or the sentence written in the DSL). - Bean Validation's built-in messages (
Size,NotNull,Min...) in the same language, with the numbers (size must be between 0 and 10/크기가 0에서 10 사이여야 합니다). A sentence written in the DSL stays as written.
The server machine's language is never used. A server on a Korean OS answers a request without a
header, en or sv in English (checked by starting the pet store with
-Duser.language=ko -Duser.country=KR).
application.yml - a request without a header in Korean
spring:
web:
locale: ko
English is the bundle's default, so Accept-Language: en stays English even with
spring.web.locale: ko. Another LocaleResolver (session, cookie, fixed) decides the language
itself.
Translating
- The build writes, per domain,
src/main/resources/i18n/<domain>-messages.properties- every code the domain can answer with its default wording. It is generated: do not edit it. - Beside it, a file of yours
<domain>-messages_ko.propertiestranslates the keys you need (UTF-8 is fine). A missing key is said in its default wording.
src/main/resources/i18n/petstore-messages_ko.properties (the pet store, excerpt)
# Korean wording of petstore-messages.properties (generated beside it).
# The keys come from that file; a key missing here is said in its default wording.
error.not-found.title=찾을 수 없음
error.invalid.title=잘못된 요청
error.state.title=지금 상태에서는 할 수 없는 요청
error.not-signed-in.title=로그인이 필요합니다
error.forbidden.title=권한이 없습니다
error.unique.title=이미 있음
error.unique=이미 사용 중인 값입니다.
invariant.Item.unitCost.min=가격은 0보다 커야 합니다
invariant.PurchaseOrder.lines.minsize=장바구니가 비어 있습니다
state.PurchaseOrder.status.cancel=주문 상태가 {0}이면 취소할 수 없습니다 (가능한 상태: {1})
state.PurchaseOrder.status.ship=주문 상태가 {0}이면 배송할 수 없습니다 (가능한 상태: {1})
# A field's check (400), by Spring's message codes: <Constraint>.<field>
Size.lines=장바구니가 비어 있습니다
NotNull.lines=장바구니가 비어 있습니다
# What Spring MVC refuses before a controller runs (RequestShapeErrors reads this file too)
error.unreadable=요청 본문이 없거나 JSON 이 아닙니다.
error.method-not-allowed={0} 메서드는 여기서 쓸 수 없습니다.
typeMismatch.lines.quantity=수량은 0 이상의 정수입니다
POST /api/petstore/purchaseOrder/{id}/cancel
Accept-Language: ko
HTTP/1.1 409
{ "title": "지금 상태에서는 할 수 없는 요청",
"detail": "주문 상태가 SHIPPED이면 취소할 수 없습니다 (가능 한 상태: [PLACED])",
"code": "state.PurchaseOrder.status.cancel", "status": 409 }
Keys
| Key | For |
|---|---|
error.<code>.title, error.<code> | the shared codes' title and sentence |
state.<Entity>.<field>.<command> | a lifecycle refusal - {0} the current state, {1} the allowed ones |
invariant.<Entity>.<field>.min ... | a field's range |
reference.<Entity>.<relation> | a missing referenced row - {0} the id |
<Constraint>.<field> (Size.lines, NotNull.customerDTO.login) | a field check (400), Spring's message codes: {0} the field, then the constraint's attributes by name (Size: {1} max, {2} min) |
typeMismatch.<field> | a value its type refuses; typeMismatch.lines[0].quantity that line, typeMismatch.lines.quantity every line |
| a code of your own | a refusal your code throws with that code |
- In sentences with values (
state.*,reference.*) write'as''(JavaMessageFormat). - Do not write codes without a field (
Size=,NotNull=): they override every DSL sentence of that constraint in that language. - Do not set the language with
ValidationMessages*.properties: a default file overrides Hibernate Validator's translations, and a_<lang>file alone makes other languages fall back to the machine's language. The rules above already give one language per answer. - The lookup order:
messages_<lang>.propertieson the classpath, the domain bundles and the translations beside them, the application'sMessageSource, the default wording. A translation of a domain code belongs inmessages_<lang>.propertiesor beside the domain bundle - a bundle moved withspring.messages.basenameis read after the domain bundles.
Translating a refusal your code throws
throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn);
i18n/library-messages_ko.properties (yours) or messages_ko.properties
loan.no-copy-left=빌려줄 수 있는 {0} 이(가) 없습니다
The default wording is in the code - not in the generated <domain>-messages.properties, which is
rewritten on every build.