How I stopped my README.md and README.zh.md from drifting apart
Source: Dev.to
The drift problem
Every project that ships a translated README follows the same lifecycle:
- Someone writes
README.mdin English. - A contributor opens a PR with
README.zh.md. Great. - Months later, English has new sections while the Chinese version still reflects the original.
- A second translator adds
README.es.md. Which version should they translate from? The currentREADME.mdor the staleREADME.zh.md?
After a while you end up with several READMEs that disagree on what the project actually is. It’s hard to tell at a glance which file is stale, reviewers don’t read all three, and translations rot because nothing forces them to stay in sync.
Solution: NRG
I built a small Java tool—NRG (README Generator)—to keep translated READMEs in lockstep.
- Write one
README.src.md. - Run NRG and get back
README.md,README.zh.md,README.ja.md, and any other language variants you list.
The template lines fall into three categories:
| Category | Description |
|---|---|
| Shared | Appears in every output (badges, code blocks, file paths, anything language‑agnostic). No markup needed. |
| Language‑tagged | Appears only in the specified language’s output. |
| Inline per‑language | Useful for short strings like anchor names or button labels where a full line per language would be overkill. |
Example template snippet

This library is small but hard to use.
Эта библиотека маленькая, но сложная в использовании.
本库虽小,但难以使用。
## ${en:'Table of contents', ru:'Содержание', zh:'目录'}
Running the generator:
nrg -f README.src.md
produces the language‑specific files, each stamped with a header comment indicating that the file is generated.
CI integration
NRG ships a GitHub Action (nanolaba/nrg-action@v1) with a check mode. On every PR it:
- Regenerates the READMEs into a temporary directory.
- Diffs them against the committed versions.
If they don’t match, the build fails with a unified diff, e.g.:
--- README.md (on disk)
+++ README.md (generated)
@@ -27,7 +27,7 @@
-## Quick start
+## Getting started
Thus a contributor cannot hand‑edit README.zh.md without either updating README.src.md and regenerating, or having the CI reject the PR.
The same check can be run locally:
nrg -f README.src.md --check
which is handy for pre‑commit hooks.
Widgets
Anything the template syntax can’t express directly is a widget. The shipped set includes:
${widget:tableOfContents(ordered='true')} # auto‑builds a TOC
${widget:import(path='docs/intro.src.md')} # compose templates
${widget:exec(cmd='git rev-parse --short HEAD')} # embed shell output
${widget:fileTree(path='src/main/java', depth='2')} # generate a directory tree
${widget:math(expr='\\pi r^2')} # render LaTeX (SVG fallback)
Additional built‑ins: alert, badge, if / endIf, date, todo.
Custom widgets are implemented by creating a class that implements the NRGWidget interface—useful for project‑specific patterns such as a “feature matrix” widget.
Maven plugin
For Java projects, NRG can run automatically during the compile phase:
<plugin>
<groupId>com.nanolaba</groupId>
<artifactId>nrg-maven-plugin</artifactId>
<version>1.2</version>
<configuration>
<sourceFile>README.src.md</sourceFile>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>create-files</goal>
</goals>
</execution>
</executions>
</plugin>
GitHub Action (language‑agnostic)
The action provisions Java itself, so you don’t need a Java toolchain in your repository:
- uses: nanolaba/nrg-action@v1
with:
file: README.src.md
mode: check
Works for Python, JavaScript, Rust, or any other language ecosystem.
Additional notes
- Java 8 minimum – the binary is portable. If you dislike installing JDKs, the GitHub Action is the zero‑touch option.
- Not a translation tool – NRG synchronises structure; actual prose translation remains a human (or LLM) job.
- No Markdown AST – substitution and widgets operate on raw text. This works 99 % of the time, but a clever author could produce broken Markdown that NRG won’t catch. Use the separate
validatemode for stricter checks. - Early days – currently at v1.2, used by a handful of open‑source repos. The widget API may still evolve.
Feedback
I’m looking for honest feedback while the project is still small enough to change direction.
- How would you adopt this over your current setup (hand‑syncing, custom script, doing nothing)?
- What would make you not use it?
The repository, full documentation, and a GIF demo are available at:
Thanks for reading – happy to answer questions in the comments.