This document specifies how java.evolved supports multiple languages. Internationalization is implemented via two distinct layers:
-
UI strings layer — every piece of hard-coded copy in the templates (labels, button text, nav, footer, etc.) is extracted into a per-locale
translations/strings/{locale}.yamlfile and injected at build time. -
Content translation layer — translated pattern YAML files contain only translatable fields, stored under
translations/content/{locale}/. The generator falls back to the English file for any pattern that has not yet been translated.
English is a first-class locale. All locales — including English — go through the same build pipeline.
content/ # English content (source of truth)
language/
collections/
strings/
streams/
concurrency/
io/
errors/
datetime/
security/
tooling/
enterprise/
translations/ # All i18n artifacts
strings/
en.yaml # English UI strings (extracted from templates)
pt-BR.yaml # Partial — missing keys fall back to en.yaml
ja.yaml
content/
pt-BR/
language/
type-inference-with-var.yaml # Translated YAML (translatable fields only)
collections/
strings/
streams/
concurrency/
io/
errors/
datetime/
security/
tooling/
enterprise/
ja/
language/
...
templates/ # Templates use {{…}} tokens for every UI string
slug-template.html
index.html
...
html-generators/
locales.properties # Ordered list of supported locales + display names
generate.java # Extended to iterate all locales
site/ # Generated output
index.html # English home (path = /)
language/
type-inference-with-var.html
data/
snippets.json
pt-BR/
index.html
language/
type-inference-with-var.html
data/
snippets.json
ja/
...
# html-generators/locales.properties
# format: locale=Display name (first entry is the default/primary locale)
en=🇬🇧 English
de=🇩🇪 Deutsch
es=🇪🇸 Español
pt-BR=🇧🇷 Português (Brasil)
zh-CN=🇨🇳 中文 (简体)
ar=🇸🇦 العربية
fr=🇫🇷 Français
ja=🇯🇵 日本語
ko=🇰🇷 한국어The generator reads this file to know which locales to build and what label to show in the language selector.
Every user-visible string in the templates is assigned a dot-separated key. The English file is the complete reference; locale files are partial and only need to include keys that differ from English.
# translations/strings/en.yaml
site:
title: java.evolved
tagline: Java has evolved. Your code can too.
tagline_line1: Java has evolved.
tagline_line2: Your code can too.
description: A collection of modern Java code snippets. Every old Java pattern next
to its clean, modern replacement — side by side.
heroSnippetCount: '✦ {{snippetCount}} modern patterns · Java 8 → Java 25'
heroOld: Old
heroModern: Modern
allComparisons: All comparisons
snippetsBadge: '{{snippetCount}} snippets'
nav:
allPatterns: ← All patterns
toggleTheme: Toggle theme
viewOnGitHub: View on GitHub
selectLanguage: Select language
breadcrumb:
home: Home
sections:
codeComparison: Code Comparison
whyModernWins: Why the modern way wins
oldApproach: Old Approach
modernApproach: Modern Approach
sinceJdk: Since JDK
difficulty: Difficulty
jdkSupport: JDK Support
howItWorks: How it works
relatedDocs: Related Documentation
relatedPatterns: Related patterns
filters:
show: 'Show:'
all: All
difficulty:
beginner: Beginner
intermediate: Intermediate
advanced: Advanced
search:
placeholder: Search snippets…
noResults: No results found.
esc: ESC
searchTrigger: Search…
navigate: navigate
open: open
close: close
cards:
old: Old
modern: Modern
hoverHint: hover to see modern →
hoverHintRelated: Hover to see modern ➜
touchHint: 👆 tap or swipe →
copy:
copy: Copy
copied: Copied!
share:
label: Share
view:
expandAll: Expand All
collapseAll: Collapse All
stats:
modernPatterns: Modern Patterns
jdkVersions: JDK Versions Covered
categories: Categories
linesOfPython: Lines of Python Required
footer:
tagline: Java has evolved. Your code can too.
madeWith: Made with ❤️ by
and: and
inspiredBy: Inspired by
viewOnGitHub: View on GitHub
copilot:
headline: Modernize your Java codebase with GitHub Copilot.
description: Let Copilot help you migrate legacy patterns to modern Java — automatically.
appModernization: App Modernization →
javaGuide: Java Guide →
support:
available: Available
preview: Preview
experimental: Experimental
contribute:
button: Contribute
codeIssue: Report a code issue
translationIssue: Report a translation issue
suggestPattern: Suggest a new pattern
seeIssue: "See a problem with this code?"
reportIt: "Let us know."
untranslated:
notice: This page has not yet been translated into {{localeName}}.
viewInEnglish: View in English# translations/strings/pt-BR.yaml (partial — only translated keys required)
site:
tagline: O Java evoluiu. Seu código também pode.
description: Uma coleção de snippets modernos de Java...
nav:
allPatterns: ← Todos os padrões
toggleTheme: Alternar tema
sections:
codeComparison: Comparação de código
whyModernWins: Por que a forma moderna ganha
howItWorks: Como funciona
relatedDocs: Documentação relacionada
relatedPatterns: Padrões relacionadosKey-level fallback rule: if a key present in en.yaml is absent from a
locale file, the generator uses the English value and emits a build-time warning:
[WARN] pt-BR.yaml: missing key "footer.madeWith" — using English fallback
The page is always rendered completely; no key is ever silently blank. The warning is purely informational and does not abort the build.
Translated content files are partial — they contain only the translatable fields. The generator overlays them onto the English base at build time. This prevents translators from diverging structural data (code, navigation, metadata) from the English source of truth.
Every field in a slug definition file falls into one of three categories:
| Category | Fields | Rule |
|---|---|---|
| Translate (include in translation file) | title, summary, explanation, oldApproach, modernApproach, whyModernWins (full array), support.description |
These are the only fields present in a translation file |
| English source of truth (never in translation file) | id, slug, category, difficulty, jdkVersion, oldLabel, modernLabel, oldCode, modernCode, prev, next, related, docs |
Always taken from the English content file; any values in the translation file are ignored |
| Translated via UI strings | difficulty, support.state |
Enum values stay in English; display names resolved from translations/strings/{locale}.yaml at build time |
Why enum fields use UI strings instead of content translation:
Fields like difficulty (beginner, intermediate, advanced) and
support.state (available, preview, experimental) are enum values used
programmatically as CSS classes, filter keys, and data attributes. Their
display names are resolved at build time from the UI strings layer:
difficulty→difficulty.beginner,difficulty.intermediate,difficulty.advancedsupport.state→support.available,support.preview,support.experimentalcategory→ display name fromcategories.properties
This separation ensures enum values remain stable across locales while display text is centrally managed and consistently translated.
Other non-translated fields:
whyModernWins[*].icon— emoji; typically kept as-is across localesdocs[*].title— kept in English since the linked documentation is English
Translation files contain only translatable fields — no structural data:
# translations/content/pt-BR/language/type-inference-with-var.yaml
title: Inferência de tipo com var
oldApproach: Tipos explícitos
modernApproach: Palavra-chave var
summary: Use var para inferência de tipo em variáveis locais — menos ruído, mesma
segurança.
explanation: Desde o Java 10, o compilador infere os tipos de variáveis locais a partir
do lado direito da atribuição. Isso reduz o ruído visual sem sacrificar a segurança
de tipos. Use var quando o tipo for óbvio pelo contexto.
whyModernWins:
- icon: ⚡
title: Menos boilerplate
desc: Não é necessário repetir tipos genéricos complexos em ambos os lados da atribuição.
- icon: 👁
title: Melhor legibilidade
desc: Foco nos nomes de variáveis e valores, não nas declarações de tipo.
- icon: 🔒
title: Ainda seguro em tipos
desc: O compilador infere e impõe o tipo exato em tempo de compilação.
support:
description: Amplamente disponível desde o JDK 10 (março de 2018)For each pattern and locale the generator:
- Loads the English baseline from
content/<cat>/<slug>.json(or.yaml/.yml). - Checks whether
translations/content/<locale>/<cat>/<slug>.{json,yaml,yml}exists.- Yes → deep-copies the English node, then overlays only the translatable
fields (
title,summary,explanation,oldApproach,modernApproach,whyModernWins,support.description) from the translation file. - No → use the English file and inject an "untranslated" banner (see next section).
- Yes → deep-copies the English node, then overlays only the translatable
fields (
- Loads
translations/strings/<locale>.yamldeep-merged overen.yaml. Any key present inen.yamlbut absent from the locale file falls back to the English value; the generator logs a[WARN]for each missing key and continues without aborting. - Renders the template, substituting content tokens (
{{title}}, …) and UI-string tokens ({{nav.allPatterns}}, …). - Writes output to
site/<locale>/<cat>/<slug>.html(orsite/<cat>/<slug>.htmlfor English).
When falling back to English content for a non-English locale, the generator injects:
<div class="untranslated-notice" lang="en">
This page has not yet been translated into Português (Brasil).
<a href="/language/type-inference-with-var.html">View in English</a>
</div>The banner is suppressed when the locale is en or a translation file exists.
Every hard-coded English string in the templates is replaced with a token whose
name mirrors the dot-separated key path in strings/{locale}.yaml:
| Before | After |
|---|---|
Code Comparison |
{{sections.codeComparison}} |
Why the modern way wins |
{{sections.whyModernWins}} |
How it works |
{{sections.howItWorks}} |
← All patterns |
{{nav.allPatterns}} |
Copy |
{{copy.copy}} |
Copied! |
{{copy.copied}} |
Search patterns… |
{{search.placeholder}} |
The <html> opening tag becomes <html lang="{{locale}}">.
hreflang alternate links are generated for every supported locale:
<link rel="alternate" hreflang="en" href="https://javaevolved.github.io/language/type-inference-with-var.html">
<link rel="alternate" hreflang="pt-BR" href="https://javaevolved.github.io/pt-BR/language/type-inference-with-var.html">
<link rel="alternate" hreflang="x-default" href="https://javaevolved.github.io/language/type-inference-with-var.html">A globe icon button (🌐) is placed in the nav bar immediately next to the
dark/light theme toggle button. Clicking it opens a dropdown list of available
locales. The list is rendered at build time from locales.properties.
<!-- inside .nav-right, adjacent to the existing theme-toggle button -->
<div class="locale-picker" id="localePicker">
<button type="button" class="locale-toggle" aria-haspopup="listbox" aria-expanded="false"
aria-label="Select language">🌐</button>
<ul role="listbox" aria-label="Language">
<li role="option" data-locale="en" aria-selected="true">English</li>
<li role="option" data-locale="pt-BR" aria-selected="false">Português (Brasil)</li>
</ul>
</div>When the user selects a locale, app.js rewrites the current URL path to the
equivalent page for that locale and persists the choice to localStorage so
subsequent page loads open in the correct language automatically.
The dropdown is hidden by default and toggled via the .locale-toggle button.
Clicking outside the dropdown or pressing Escape closes it. The currently
active locale's <li> receives aria-selected="true" and a visual highlight.
The search index path and locale picker must both be locale-aware:
// Detect current locale from path prefix
const locale = location.pathname.startsWith('/pt-BR/') ? 'pt-BR'
: location.pathname.startsWith('/ja/') ? 'ja'
: 'en';
// Load the correct snippets index
const indexPath = locale === 'en'
? '/data/snippets.json'
: `/${locale}/data/snippets.json`;Localised strings consumed by JavaScript are embedded as a <script> block by
the generator so app.js doesn't need to fetch them separately:
<script>
window.i18n = {
searchPlaceholder: "Buscar padrões...",
noResults: "Nenhum resultado encontrado.",
copied: "Copiado!"
};
</script>app.js reads from window.i18n instead of hard-coded literals.
The deploy workflow iterates all entries in locales.properties:
- name: Build site
run: jbang html-generators/generate.java --all-localesOr explicitly, to support incremental locale addition:
- name: Build site
run: |
jbang html-generators/generate.java
jbang html-generators/generate.java --locale pt-BR
jbang html-generators/generate.java --locale jaWhen a new slug is added, AI generates translations automatically:
New English slug → AI prompt → Translated YAML file → Schema validation → Commit
- The AI receives the full English content and returns a translated YAML file — no special field-filtering rules in the prompt.
oldCode/modernCodeare overwritten by the build tooling, so AI can copy them verbatim without risk of hallucinated code shipping to users.- The translated file passes the same schema validation as English files — no separate validation logic needed.
- If the AI file does not exist yet, the fallback is an explicit "untranslated" banner rather than a silent gap.
- Trigger — GitHub Actions detects a new or modified
content/<cat>/<slug>.json(push event or workflow dispatch). - Translate — For each supported locale, call the translation model with:
See the Field Translation Reference table above for the full rationale.
Translate the following Java pattern from English to {locale}. Return a YAML file containing ONLY these translated fields: - title, summary, explanation, oldApproach, modernApproach - whyModernWins (full array with icon, title, desc) - support.description (inside a "support" object) Do NOT include: slug, id, category, difficulty, jdkVersion, oldLabel, modernLabel, oldCode, modernCode, docs, related, prev, next, support.state. Return valid YAML only. - Validate — Verify the output contains only translatable fields.
- Commit — Write the output to
translations/content/{locale}/<cat>/<slug>.yamland commit. - Deploy — The generator picks it up on next build; the "untranslated" banner disappears automatically.
When an English file is modified, the same automation regenerates the translated file or opens a PR flagging the diff for human review. Since translation files only contain translatable text, structural changes to the English file (code, navigation, metadata) take effect immediately without needing to update the translation.
Translation text frequently contains colons (:) — especially in Romance
languages with explanatory phrases like "Nota: el resultado es inmutable" or
"Ejemplo: seed es el inicio". Unquoted colons break YAML parsing.
Always ensure values containing colons are properly quoted:
# ✗ BROKEN — YAML interprets "Nota:" as a mapping key
explanation: Stream.toList() devuelve una lista. Nota: el resultado es inmutable.
# ✓ CORRECT — value is quoted
explanation: "Stream.toList() devuelve una lista. Nota: el resultado es inmutable."When generating translation files programmatically, use a YAML library's
dump() function (which handles quoting automatically) rather than writing
raw YAML strings by hand.
Jackson's YAML parser (jackson-dataformat-yaml) is stricter than Python's
PyYAML. Certain serialization styles that Python accepts can crash Jackson:
---document markers combined with backslash line continuations (\) can triggerArrayIndexOutOfBoundsExceptionin Jackson'sUTF8Reader.- Always test translation files with
jbang html-generators/generate.javabefore committing — Python validation alone is not sufficient.
If a file parses in Python but fails in Jackson, re-serialize it with
yaml.dump(..., default_flow_style=False) to produce a compatible style.
Always validate translation files before committing:
# Build all locales — will fail fast on any broken file
jbang html-generators/generate.java
# Or build a single locale for faster iteration
jbang html-generators/generate.java --locale esBoth content translations and UI strings files use YAML format (.yaml).
The generator also supports JSON (.json) and .yml extensions as fallbacks,
discovering files by trying .json, .yaml, and .yml extensions in order
via findWithExtensions(). In practice, all current files use .yaml.
| Phase | Work |
|---|---|
| 1 | Extract every hard-coded string from templates into translations/strings/en.yaml; replace literals with {{…}} tokens; verify English output is unchanged |
| 2 | Add locales.properties; extend generator to load strings, support --locale, and fall back gracefully |
| 3 | Add language selector to nav; implement app.js locale detection and path rewrite |
| 4 | Translate strings/pt-BR.yaml and 2–3 content files as a proof-of-concept; verify fallback banner |
| 5 | Update GitHub Actions; add hreflang alternate links |
| 6 | Wire up AI translation automation; add translations/ schema documentation |