Skip to content

chore: update compare-translations.ts logic#1063

Open
userquin wants to merge 7 commits intonpmx-dev:mainfrom
userquin:update-compare-translations-script
Open

chore: update compare-translations.ts logic#1063
userquin wants to merge 7 commits intonpmx-dev:mainfrom
userquin:update-compare-translations-script

Conversation

@userquin
Copy link
Contributor

@userquin userquin commented Feb 6, 2026

This PR includes:

  • merge json files for country variants when loading json
  • export mergeLocaleObject utility from lunaria/prepare-json-files.ts and use it
  • protect new locales files: will prevent adding new locales without the country in the file name when not registered at country variants in config/i18n.ts
  • protect new locales for country variants not registered at country variants in config/i18n.ts

Context: check autofix in this PR #1036

/cc @shuuji3

@vercel
Copy link

vercel bot commented Feb 6, 2026

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

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Feb 6, 2026 3:26am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Feb 6, 2026 3:26am
npmx-lunaria Ignored Ignored Feb 6, 2026 3:26am

Request Review

@codecov
Copy link

codecov bot commented Feb 6, 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 Feb 6, 2026

📝 Walkthrough

Walkthrough

Adds an exported function mergeLocaleObject(locale: LocaleObject, copy = false) to lunaria/prepare-json-files.ts that returns a merged JSON source for a locale or copies the source when copy is true; mergeLocale now uses this function and writes the result. Internal helpers loadJsonFile and getFileName were moved and kept non-exported. prepareJsonFiles now has an explicit Promise<void> return type. scripts/compare-translations.ts was refactored to async/ESM patterns: introduces LocaleInfo, country-variant mapping and validation (checkCountryVariant, checkJsonName), makes loading/processing async (loadJson, processLocale, run*), and omits empty nested objects during sync/fix.

Suggested reviewers

  • danielroe
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The pull request description is directly related to the changeset, detailing specific functionality additions including JSON file merging, utility exports, and locale file protection mechanisms.

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

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

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lunaria/prepare-json-files.ts (2)

40-61: ⚠️ Potential issue | 🔴 Critical

mergeLocaleObject returns undefined for single-file locales — root cause of the pipeline failure.

Two issues in this function:

  1. Returns undefined for single-file locales (lines 44–46): The function copies the file as a side effect but returns nothing, which causes the TypeError [ERR_INVALID_ARG_TYPE] crash in mergeLocale (line 73) and will also break loadJson in compare-translations.ts (line 110).
  2. fs.cp side effect (line 45): A function named mergeLocaleObject that's exported as a pure data utility should not perform file-system writes. The copy belongs in the wrapper mergeLocale.
🐛 Proposed fix — return parsed JSON for all paths; move copy to mergeLocale
 export async function mergeLocaleObject(locale: LocaleObject) {
   const files = locale.files ?? []
   if (locale.file || files.length === 1) {
     const json = locale.file ?? (files[0] ? getFileName(files[0]) : undefined)
     if (!json) return
-    await fs.cp(path.resolve(`${localesFolder}/${json}`), path.resolve(`${destFolder}/${json}`))
-    return
+    return await loadJsonFile(json)
   }
 
   const firstFile = files[0]
   if (!firstFile) return
   const source = await loadJsonFile(getFileName(firstFile))
   let currentSource: unknown
   for (let i = 1; i < files.length; i++) {
     const file = files[i]
     if (!file) continue
     currentSource = await loadJsonFile(getFileName(file))
     deepCopy(currentSource, source)
   }
 
   return source
 }

71-77: ⚠️ Potential issue | 🔴 Critical

mergeLocale does not guard against undefined from mergeLocaleObject.

Even after fixing the upstream function, early-return paths (e.g. no files at all) can still yield undefined. JSON.stringify(undefined) produces the JS value undefined, which fs.writeFile rejects with ERR_INVALID_ARG_TYPE. Add a guard here.

🛡️ Proposed fix
 async function mergeLocale(locale: LocaleObject) {
   const source = await mergeLocaleObject(locale)
+  if (source === undefined) return
   await fs.writeFile(
     path.resolve(`${destFolder}/${locale.code}.json`),
     JSON.stringify(source, null, 2),
   )
 }
🧹 Nitpick comments (3)
scripts/compare-translations.ts (3)

33-41: Unchecked array destructuring from split.

locale.split('-') returns string[], so both lang and country are string | undefined at runtime (depending on noUncheckedIndexedAccess). lang is assigned to LocaleInfo.lang: string without a guard. While split always returns at least one element in practice, the coding guidelines require explicit checks when accessing array values by index.

🛡️ Proposed fix
 const extractLocalInfo = (
   filePath: string,
   forCountry: boolean = false,
   mergeLocale: boolean = false,
 ): LocaleInfo => {
   const locale = basename(filePath, '.json')
-  const [lang, country] = locale.split('-')
-  return { filePath, locale, lang, country, forCountry, mergeLocale }
+  const parts = locale.split('-')
+  const lang = parts[0]
+  if (!lang) {
+    throw new Error(`Invalid locale file name: "${locale}"`)
+  }
+  const country = parts[1]
+  return { filePath, locale, lang, country, forCountry, mergeLocale }
 }

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


138-144: delete result[key] is a no-op — key was never assigned to result.

At this point in the loop, result[key] has not yet been set, so delete result[key] does nothing. A continue (or simply not assigning) would express the intent more clearly.

♻️ Suggested simplification
       const data = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath)
       // don't add empty objects: --fix will prevent this
-      if (Object.keys(data).length === 0) {
-        delete result[key]
-      } else {
+      if (Object.keys(data).length > 0) {
         result[key] = data
       }

330-342: populateLocaleCountries() is called after loading the reference — consider calling it earlier.

Currently the reference content is loaded at line 332 via loadJson, which internally calls checkJsonNamecheckCountryVariant → reads from contries. Because the reference LocaleInfo has mergeLocale: undefined (falsy), it bypasses the merge path and the contries lookup doesn't affect it. However, this ordering is fragile — if the reference loading ever switches to the merge path, the contries map will be empty.

Moving populateLocaleCountries() before loadJson would make the initialisation order explicit and safe.

♻️ Suggested reordering
 const run = async (): Promise<void> => {
   const referenceFilePath = join(LOCALES_DIRECTORY, REFERENCE_FILE_NAME)
+
+  populateLocaleCountries()
+
   const referenceContent = await loadJson({
     filePath: referenceFilePath,
     locale: 'en',
     lang: 'en',
   })
 
   const args = process.argv.slice(2)
   const fix = args.includes('--fix')
   const targetLocale = args.find(arg => !arg.startsWith('--'))
 
-  populateLocaleCountries()
-
   if (targetLocale) {

@userquin
Copy link
Contributor Author

userquin commented Feb 6, 2026

somethings wrong with cs-CZ.json file => wrong logic, we use fs.cp to copy the file, fixed

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: 1

🧹 Nitpick comments (6)
lunaria/prepare-json-files.ts (2)

40-67: Implicit return type is Promise<void | unknown> — consider narrowing.

mergeLocaleObject can return undefined (lines 46, 49), a parsed JSON object (line 51), or the merged source (line 66). The callers in compare-translations.ts cast the result with as NestedObject. An explicit return type (e.g. Promise<Record<string, unknown> | undefined>) would make the contract clearer and help catch misuse at compile time.


69-71: loadJsonFile returns any from JSON.parse.

This is a minor type-safety gap. Returning unknown (or Record<string, unknown>) instead of the implicit any would propagate stricter checks to all callers.

♻️ Proposed fix
-async function loadJsonFile(name: string) {
-  return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8'))
+async function loadJsonFile(name: string): Promise<Record<string, unknown>> {
+  return JSON.parse(await fs.readFile(path.resolve(`${localesFolder}/${name}`), 'utf8')) as Record<string, unknown>
 }

As per coding guidelines, **/*.{ts,tsx,vue}: "Ensure you write strictly type-safe code".

scripts/compare-translations.ts (4)

43-58: extractLocalInfo is called with locale codes, not file paths.

Lines 51–54 pass bare strings like "en" or "en-US" as the filePath argument to extractLocalInfo. The filePath field in the resulting LocaleInfo then holds a locale code rather than an actual path. This doesn't cause a runtime bug today because filePath from these map entries is never read, but it's semantically misleading and fragile if anyone later relies on contries entries having a real filePath.

Consider either renaming the parameter / field to something like localeOrPath, or passing a proper path.


150-156: delete result[key] on line 153 is a no-op — simplify the branch.

At this point result[key] has never been assigned, so delete result[key] does nothing. The intent (skip empty nested objects) is already achieved by only assigning in the else branch. Simplifying removes dead code and makes the intent clearer.

♻️ Proposed fix
       const data = syncLocaleData(refValue, nextTarget, stats, fix, propertyPath)
       // don't add empty objects: --fix will prevent this
-      if (Object.keys(data).length === 0) {
-        delete result[key]
-      } else {
+      if (Object.keys(data).length > 0) {
         result[key] = data
       }

288-288: Prefer optional chaining over ! assertion on results[0].

The ternary guard ensures results.length > 0, but optional chaining is more idiomatic and equally concise:

♻️ Proposed fix
-  const referenceKeysCount = results.length > 0 ? results[0]!.referenceKeys.length : 0
+  const referenceKeysCount = results[0]?.referenceKeys.length ?? 0

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


342-361: Reference locale LocaleInfo hard-codes locale: 'en' — fragile coupling.

The reference file is en.json (line 10, REFERENCE_FILE_NAME), but the LocaleInfo at line 347–348 hard-codes locale: 'en' and lang: 'en' independently. If REFERENCE_FILE_NAME ever changes, these literals would silently drift. Consider deriving both values from the constant:

const referenceLocale = basename(REFERENCE_FILE_NAME, '.json')

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: 1

🧹 Nitpick comments (2)
lunaria/prepare-json-files.ts (2)

40-43: Return type Promise<void | unknown> collapses to Promise<unknown> — consider a narrower signature.

void | unknown is equivalent to unknown in TypeScript's type system, so the void conveys no extra compile-time safety. Callers (like mergeLocale) rely on a falsy check to distinguish "no result" from "has data", which works at runtime but isn't enforced by the type.

A more descriptive return type would make the contract clearer:

♻️ Suggested improvement
 export async function mergeLocaleObject(
   locale: LocaleObject,
   copy = false,
-): Promise<void | unknown> {
+): Promise<Record<string, unknown> | undefined> {

This would require a small cast/assertion when returning the result of loadJsonFile / building source, but it gives callers proper type narrowing with if (source).


80-91: mergeLocale silently succeeds when mergeLocaleObject returns a falsy-but-valid value.

The guard if (!source) on Line 82 would also skip writing if source happened to be an empty object {} — wait, no, !{} is false, so an empty object would pass. The actual concern is that any falsy value (e.g. 0, "", false) returned from mergeLocaleObject would be skipped. In practice this won't happen because JSON locale files always parse to objects, but a === undefined check would be more precise and intention-revealing.

♻️ Suggested tightening
   const source = await mergeLocaleObject(locale, true)
-  if (!source) {
+  if (source === undefined) {
     return
   }

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