Skip to content

feat(i18n): add en.meta.json generation script#1761

Open
shuuji3 wants to merge 6 commits intonpmx-dev:mainfrom
shuuji3:feat/generate-en-meta-json
Open

feat(i18n): add en.meta.json generation script#1761
shuuji3 wants to merge 6 commits intonpmx-dev:mainfrom
shuuji3:feat/generate-en-meta-json

Conversation

@shuuji3
Copy link
Member

@shuuji3 shuuji3 commented Feb 28, 2026

🔗 Linked issue

#1747

🧭 Context

Currently, there is no mechanism to detect changes in en.json. Previously, we had to choose one of the following approaches when English text needed an update:

  • Using a different key: Even a small change requires re-translation for all other languages.
  • Updating the existing text: It is difficult to track which keys were updated, making it hard to sync with other languages.

This first step records metadata, tracking which commit updated each English key, which allows us to monitor changes in the English locale file effectively.

📚 Description

Implemented the first two tasks for automated en.json change detection:

  • Added a new generation script:
    • This script generates or updates en.meta.json alongside en.json.
    • If en.json is changed (added, removed, or modified) since the previous update, the corresponding values in en.meta.json are updated with the latest commit hash and the new string value.
  • Added a GitHub Actions workflow:
    • The script is triggered when a new commit is pushed to the main branch, only when the commit modifies i18n/locales/en.json.
    • This is needed to record a commit hash in main branch history.
      • I also considered and experimented to add auto-commit during the PR or local branch, but it cannot find a reliable commit hash if PR is squash-merged.
      • It could record PR number but I wanted to be git platform agnostic.

The example auto-commit of en.meta.json can be found in this CI test branch: https://github.com/shuuji3/npmx.dev/commits/feat/generate-en-meta-json-ci-test
(Note: In the test branch, I pushed directly to main branch and lunaria/files/* is included. But they are usually merged along with PR, so they should not be included in auto-commit)

You can also check the Vitest file to see how this script records commit hash and text.

@vercel
Copy link

vercel bot commented Feb 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
docs.npmx.dev Ready Ready Preview, Comment Mar 2, 2026 6:25am
npmx.dev Ready Ready Preview, Comment Mar 2, 2026 6:25am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
npmx-lunaria Ignored Ignored Mar 2, 2026 6:25am

Request Review

@codecov
Copy link

codecov bot commented Feb 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d6b2a6 and c57e869.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • package.json

📝 Walkthrough

Walkthrough

Adds tooling and automation to maintain English i18n metadata. Introduces a GitHub Actions workflow that triggers on changes to i18n/locales/en.json and commits updates to i18n/locales/en.meta.json. Adds a CLI, utilities, and a script to compute en.meta.json from en.json (tracking per-key commit hashes and a $meta block), TypeScript types and tsconfig for the scripts, an npm script, unit tests for the metadata generation, and a vitest alias for resolving script imports.

Possibly related issues

  • npmx-dev/npmx.dev issue 1747 — Implements en.meta.json generation and CI automation (CLI, utils, types, tests, and workflow) described in the issue.

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description clearly explains the problem (lack of change detection in en.json), the solution implemented (metadata generation script and GitHub Actions workflow), and provides context for design decisions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
scripts/tsconfig.json (1)

8-12: Minor configuration inconsistency.

The declaration and declarationMap options have no effect when noEmit: true is set, as no output files are generated. Consider removing these redundant options for clarity.

♻️ Suggested fix
   "skipLibCheck": true,
   "noEmit": true,
   "allowImportingTsExtensions": true,
-  "declaration": true,
-  "types": ["node"],
-  "declarationMap": true
+  "types": ["node"]
scripts/i18n-meta/cli.ts (1)

10-15: Add explicit type guard for array access.

Accessing positionals[0] without a guard is not strictly type-safe. While the current logic works correctly (showing help when undefined), an explicit check aligns better with the coding guidelines.

♻️ Suggested fix
 function main() {
   const { positionals } = parseArgs({ allowPositionals: true })
+  const command = positionals[0]

-  if (positionals[0] !== 'update-en-meta') {
+  if (command !== 'update-en-meta') {
     showHelp()
     return
   }

   updateEnMetaJson()
 }

As per coding guidelines: "Ensure you write strictly type-safe code, for example by ensuring you always check when accessing an array value by index".

.github/workflows/i18n-meta.yml (1)

38-48: Consider adding error handling for the commit step.

If the pnpm i18n:meta:update-en-meta step fails or exits unexpectedly, the workflow continues to the commit step. Consider adding explicit error handling or using set -e in the shell script.

♻️ Suggested improvement
       - name: ⬆︎ Commit and Push changes
         run: |
+          set -e
           git config --global user.name "github-actions[bot]"
           git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
           git add i18n/locales/en.meta.json
scripts/i18n-meta/update-en-meta-json.ts (1)

23-27: Silent fallback to empty object when no commit hash is available.

If getCurrentCommitHash() returns null or empty, the function silently uses an empty object as metadata. This could mask issues in environments where Git isn't properly configured. Consider logging a warning.

♻️ Suggested improvement
   const currentCommitHash = getCurrentCommitHash()
+  if (!currentCommitHash) {
+    console.warn('⚠️ Could not determine current commit hash – metadata will be empty')
+  }
   const enMetaJson = currentCommitHash
     ? makeEnMetaJson(oldEnMetaJson, newEnJson, currentCommitHash)
     : ({} as EnMetaJson)
scripts/i18n-meta/git-utils.ts (1)

41-44: Use order-insensitive comparison for translation change detection.

At Line 44, JSON.stringify(oldObj) !== JSON.stringify(newObj) is key-order sensitive, so equivalent objects with different key order will be treated as changed.

Suggested fix
+function sortKeysRecursively(value: unknown): unknown {
+  if (Array.isArray(value)) {
+    return value.map(sortKeysRecursively)
+  }
+  if (value && typeof value === 'object') {
+    return Object.fromEntries(
+      Object.entries(value as Record<string, unknown>)
+        .sort(([a], [b]) => a.localeCompare(b))
+        .map(([k, v]) => [k, sortKeysRecursively(v)]),
+    )
+  }
+  return value
+}
+
 export function checkTranslationChanges(oldMeta: EnMetaJson, newMeta: EnMetaJson): boolean {
   const oldObj = omitMeta(oldMeta)
   const newObj = omitMeta(newMeta)
-  return JSON.stringify(oldObj) !== JSON.stringify(newObj)
+  return JSON.stringify(sortKeysRecursively(oldObj)) !== JSON.stringify(sortKeysRecursively(newObj))
 }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18d64e and 7ce664d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .github/workflows/i18n-meta.yml
  • package.json
  • scripts/i18n-meta/cli.ts
  • scripts/i18n-meta/git-utils.ts
  • scripts/i18n-meta/types.d.ts
  • scripts/i18n-meta/update-en-meta-json.ts
  • scripts/tsconfig.json
  • test/unit/scripts/generate-en-meta-json.spec.ts
  • vitest.config.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce664d and e6e0701.

📒 Files selected for processing (2)
  • scripts/i18n-meta/update-en-meta-json.ts
  • scripts/i18n-meta/utils.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
scripts/i18n-meta/update-en-meta-json.ts (1)

47-61: Consider adding explicit return type annotation for clarity.

The logic is sound: flattening both structures, comparing text values, and preserving or updating commit hashes as appropriate. The nullish coalescing at line 58 correctly handles new keys where lastCommit would be undefined.

One minor note: the as Record<string, string> casts at lines 47-48 are imprecise since accessing a missing key returns undefined at runtime. The current logic handles this correctly, but for stricter type-safety you could use a safer accessor pattern.

♻️ Optional: safer key access pattern
-    const lastSeenText = oldMetaFlat[`${key}.text`]
-    const lastCommit = oldMetaFlat[`${key}.commit`]
+    const lastSeenText = oldMetaFlat[`${key}.text`] as string | undefined
+    const lastCommit = oldMetaFlat[`${key}.commit`] as string | undefined

This makes the potential undefined explicit, aligning runtime behaviour with the type system.

scripts/i18n-meta/utils.ts (2)

5-7: Add explicit return type annotation for getCurrentCommitHash.

The function can return string | null (from the git helper), but this isn't explicitly declared. Adding the return type improves API clarity for callers.

♻️ Suggested improvement
-export function getCurrentCommitHash() {
+export function getCurrentCommitHash(): string | null {
   return git('rev-parse HEAD')
 }

48-50: Consider wrapping JSON.parse in try-catch for clearer error messages.

If the file contains invalid JSON, the error will propagate with a generic parse error. A wrapper could provide context about which file failed.

♻️ Optional: improved error context
 function readJson<T>(filePath: string): T {
-  return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T
+  try {
+    return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T
+  } catch (error) {
+    throw new Error(`Failed to parse JSON from ${filePath}: ${error instanceof Error ? error.message : error}`)
+  }
 }

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e6e0701 and 8d6b2a6.

📒 Files selected for processing (2)
  • scripts/i18n-meta/update-en-meta-json.ts
  • scripts/i18n-meta/utils.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant