The drift problem
Every project that ships a translated README has the same lifecycle:
- Someone writes
README.mdin English. - A contributor opens a PR with
README.zh.md. Great. - Three months later, English has six new sections. Chinese has the original.
- A second translator opens
README.es.md. Spanish gets translated from… which version? The currentREADME.md? OrREADME.zh.md, by accident, because the structure looks tidier? - By month nine, you have three READMEs that disagree on what the project actually is.
You can't tell at a glance which file is stale. Reviewers don't read all three. Translations rot, and there's nothing forcing them to stay in sync.
I got tired of this and built a small Java tool — NRG — to fix it. Looking for honest feedback while it's still small enough to change direction.
The idea: one source, N outputs
Write one README.src.md. Get back README.md, README.zh.md, README.ja.md, and as many language variants as you list.
Lines in the template fall into three categories:
Shared across all languages (badges, code blocks, file paths, anything language-agnostic) — no markup needed, the line just appears in every output:

Language-tagged — appears only in that language's output:
This library is small but hard to use.<!--en-->
Эта библиотека маленькая, но сложная в использовании.<!--ru-->
本库虽小,但难以使用。<!--zh-->
Inline per-language phrases — useful for short strings like anchor names or button labels where one full line per language would be overkill:
## ${en:'Table of contents', ru:'Содержание', zh:'目录'}
Run nrg -f README.src.md. Out come README.md, README.ru.md, README.zh.md, all stamped with a header comment so a reader knows the file is generated.
The killer feature: CI drift-check
This is the part I actually care about, and the reason "regenerate the file periodically" wasn't enough.
NRG ships a GitHub Action (nanolaba/nrg-action@v1) with a check mode. On every PR, it regenerates the READMEs into a temp dir and diffs them against what's committed. If they don't match, the build fails with a unified diff:
--- README.md (on disk)
+++ README.md (generated)
@@ line 27 @@
-## Quick start
+## Getting started
That means a contributor can't land a hand-edit to README.zh.md that bypasses the template. Either they edit README.src.md and regenerate, or CI rejects the PR. No more silent drift.
The CLI has the same flag: nrg -f README.src.md --check. Useful in pre-commit hooks.
Built-in widgets
Anything the template syntax can't express directly is a widget. The shipped set:
-
${widget:tableOfContents(ordered='true')}— auto-builds a TOC from heading levels. -
${widget:import(path='docs/intro.src.md')}— composes templates so a giant README isn't one 800-line file. -
${widget:exec(cmd='git rev-parse --short HEAD')}— embeds shell output (handy for "last built from commit X"). -
${widget:fileTree(path='src/main/java', depth='2')}— auto-generates a directory tree. -
${widget:math(expr='\\pi r^2')}— renders LaTeX, with an SVG fallback for the cases where GitHub's native MathJax silently fails. - Plus
alert,badge,if/endIf,date,todo.
Custom widgets are a one-class implementation of an NRGWidget interface — useful if you have a recurring pattern specific to your project (e.g. a "feature matrix" widget that renders a row per supported runtime).
Three integration modes
CLI — nrg -f README.src.md. Zero config beyond declaring <!--@nrg.languages=en,ru,zh--> in the template.
Maven plugin — for Java projects, hangs off the compile phase, regenerates on every build:
<plugin>
<groupId>com.nanolaba</groupId>
<artifactId>nrg-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<file><item>README.src.md</item></file>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals><goal>create-files</goal></goals>
</execution>
</executions>
</plugin>
GitHub Action — for Python, JS, Rust, or any non-Java project. The action provisions Java itself, so you don't need a Java toolchain in your repo:
- uses: nanolaba/nrg-action@v1
with:
file: README.src.md
mode: check
That's the whole setup. There's also a Java library mode if you want to embed it in some other generator pipeline.
Honest limitations
- Java 8 minimum — the binary's portable, but if you despise installing JDKs, the GitHub Action route is the only zero-touch option.
- Not a translation tool. NRG keeps structure synchronized. Actual prose translation is still a human job (or your favourite LLM).
-
No Markdown AST. Substitution and widgets operate on raw text. This is fine 99% of the time but means a clever author can produce broken Markdown that NRG won't catch — that's why there's a separate
validatemode. - Early days. Currently at v1.2, used by a handful of open-source repos. The widget API may still change.
What I'd love feedback on
- Drift-check workflow — useful safety net, or annoying friction when a translator just wants to fix a typo? Curious how this lands for people who maintain translated docs at scale.
-
Widget syntax —
${widget:tableOfContents(title='...', ordered='true')}— readable, or have I reinvented a worse Mustache? - What would actually make you adopt this over your current setup (hand-syncing, custom script, doing nothing)? "I'd never use this because…" answers are the most useful.
Repo, full docs, and the GIF demo: github.com/nanolaba/readme-generator
Thanks for reading — happy to answer questions in the comments.
This article was originally published by DEV Community and written by Alexander A..
Read original article on DEV Community