How I stopped my README.md and README.zh.md from drifting apart

Published: (May 3, 2026 at 06:18 PM EDT)
4 min read
Source: Dev.to

Source: Dev.to

The drift problem

Every project that ships a translated README follows the same lifecycle:

  1. Someone writes README.md in English.
  2. A contributor opens a PR with README.zh.md. Great.
  3. Months later, English has new sections while the Chinese version still reflects the original.
  4. A second translator adds README.es.md. Which version should they translate from? The current README.md or the stale README.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:

CategoryDescription
SharedAppears in every output (badges, code blocks, file paths, anything language‑agnostic). No markup needed.
Language‑taggedAppears only in the specified language’s output.
Inline per‑languageUseful for short strings like anchor names or button labels where a full line per language would be overkill.

Example template snippet

![CI](https://img.shields.io/github/actions/workflow/status/owner/repo/ci.yml)

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:

  1. Regenerates the READMEs into a temporary directory.
  2. 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 validate mode 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.

0 views
Back to Blog

Related posts

Read more »

Making my own framework. Any tips?

!Cover image for Making my own framework. Any tips?https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fde...