Skip to content

Comments

🐛 fix: Preserve numeric string keys as objects instead of arrays#164

Merged
canisminor1990 merged 1 commit intolobehub:masterfrom
snomiao:sno-fix-array-key
Feb 15, 2026
Merged

🐛 fix: Preserve numeric string keys as objects instead of arrays#164
canisminor1990 merged 1 commit intolobehub:masterfrom
snomiao:sno-fix-array-key

Conversation

@snomiao
Copy link
Contributor

@snomiao snomiao commented Feb 8, 2026

Summary

Fixes the issue where objects with numeric string keys (e.g., {"0": {...}, "1": {...}}) were being converted to arrays during i18n translation processing.

Root Cause

The lodash.set() function interprets numeric string keys as array indices, which caused:

  • Objects like {"0": "value1", "1": "value2"} to become arrays ["value1", "value2"]
  • This broke downstream tools expecting object structures (e.g., vue-i18n path resolution)

Solution

Implemented a custom setByPath() function that:

  • Always treats keys as object properties, regardless of whether they look numeric
  • Preserves the original object structure during translation chunk building
  • Passes all 167 existing tests + 11 new tests

Changes

  • Added packages/lobe-i18n/src/utils/setByPath.ts - Custom path setter that preserves numeric keys
  • Added packages/lobe-i18n/src/utils/setByPath.test.ts - Comprehensive tests
  • Modified packages/lobe-i18n/src/utils/diffJson.ts - Use custom setter instead of lodash

Test Plan

  • All existing tests pass (167/167)
  • New unit tests for setByPath() (11/11)
  • Reproduction tests verify numeric keys remain as objects
  • Complex nested numeric keys preserved
  • Type checking passes across all packages

Related

Before/After

Before:

{
  "nodeDefs": ["node1", "node2"]  // ❌ Array
}

After:

{
  "nodeDefs": {  // ✅ Object
    "0": "node1",
    "1": "node2"
  }
}

🤖 Generated with Claude Code

Summary by Sourcery

Preserve numeric string keys as object properties during i18n diff processing instead of having them coerced into arrays.

Bug Fixes:

  • Fix incorrect conversion of objects with numeric string keys into arrays during i18n translation diff building.

Enhancements:

  • Introduce a custom path-setting utility that consistently treats all keys as object properties to maintain locale object structure.

Tests:

  • Add unit tests for the new path-setting utility and its handling of numeric string keys in nested structures.

Fixed issue where objects with numeric string keys (e.g., {"0": {...}, "1": {...}})
were being converted to arrays during i18n translation processing.

The root cause was lodash set() treating numeric string keys as array indices.
Implemented custom setByPath() function that always treats keys as object properties.

Related: Comfy-Org/ComfyUI_frontend#8718

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@sourcery-ai
Copy link

sourcery-ai bot commented Feb 8, 2026

Reviewer's Guide

Replaces lodash’s generic set() usage in i18n JSON diffing with a custom path setter that always treats keys as object properties, ensuring numeric string keys are preserved as objects rather than coerced to array indices, and adds focused tests for this behavior.

Class diagram for setByPath utility and diffJson module

classDiagram
  class DiffJsonUtils {
    +diff(oldLocale: LocaleObj, newLocale: LocaleObj, keyStyle: KeyStyle, config: I18nConfig) LocaleObj
    +resolveDiffPath(entry: LocaleObj, path: DiffPath, keyStyle: KeyStyle) DiffPath
    -hasOwnKey(obj: LocaleObj, key: string) boolean
  }

  class SetByPathUtils {
    +setByPath(obj: any, path: Array~string_or_number (string or number)~, value: any) void
  }

  class LocaleObj
  class I18nConfig
  class KeyStyle
  class DiffPath {
    +path: string_or_Array_of_number_or_string (string | Array<number|string>)
  }

  DiffJsonUtils --> SetByPathUtils : uses
  DiffJsonUtils ..> LocaleObj
  DiffJsonUtils ..> I18nConfig
  DiffJsonUtils ..> KeyStyle
  DiffJsonUtils ..> DiffPath
Loading

Flow diagram for diff JSON processing using setByPath

flowchart TD
  Caller["Call diff(oldLocale, newLocale, keyStyle, config)"] --> DiffFn

  subgraph DiffModule["diffJson.ts"]
    DiffFn["Function diff"]
    JustDiff["justDiff diff"]
    ResolvePath["Function resolveDiffPath"]
    ExtraObj["extra locale object"]
  end

  subgraph SetByPathModule["setByPath.ts"]
    SetByPath["Function setByPath(obj, pathArray, value)"]
  end

  DiffFn --> JustDiff
  JustDiff -->|"operations: add, remove, update"| DiffFn

  DiffFn -->|"for each add item"| ResolvePath
  ResolvePath -->|"DiffPath or string"| DiffFn

  DiffFn -->|"normalize to pathArray"| PathArray["Path as array of keys"]
  PathArray --> SetByPath

  SetByPath -->|"walks path, creates intermediate objects"| ExtraObj
  SetByPath -->|"sets finalKey as string"| ExtraObj

  ExtraObj -->|"returned as extra"| Caller
Loading

File-Level Changes

Change Details Files
Introduce a custom setByPath utility that preserves numeric string keys as object keys instead of array indices.
  • Implement setByPath(obj, path, value) that iterates a path array, stringifies each segment, creates missing intermediate containers as plain objects, and assigns the final value using a string key.
  • Ensure all intermediate containers are forced to objects (not arrays) whenever a segment is missing or is a non-object value, preventing accidental array creation based on numeric-looking keys.
packages/lobe-i18n/src/utils/setByPath.ts
Wire the new setByPath into JSON diff generation to avoid lodash.set’s array semantics.
  • Remove lodash-es set import from diffJson and keep only cloneDeep and unset.
  • For each added diff item, normalize the resolved path to an array of segments, then call setByPath(extra, pathArray, item.value) instead of lodash’s set(), preserving numeric string keys as object properties.
  • Document the rationale inline with a comment referencing numeric string key preservation.
packages/lobe-i18n/src/utils/diffJson.ts
Add targeted tests validating setByPath behavior, especially with numeric and nested keys.
  • Create a dedicated test suite for setByPath, covering flat numeric string keys, nested paths, mixed string/number segments, and cases where intermediate values need to be converted or initialized as objects.
  • Include reproduction-style tests ensuring that previously array-shaped outputs (e.g., nodeDefs) now remain objects keyed by numeric strings.
packages/lobe-i18n/src/utils/setByPath.test.ts

Possibly linked issues

  • #(not provided): PR adds setByPath to keep numeric string keys as objects, directly fixing the array conversion bug reported.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@lobehubbot
Copy link
Member

👍 @snomiao

Thank you for raising your pull request and contributing to our Community
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
If you encounter any problems, please feel free to connect with us.
非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。
如果您遇到任何问题,请随时与我们联系。

@snomiao snomiao marked this pull request as ready for review February 8, 2026 07:38
Copilot AI review requested due to automatic review settings February 8, 2026 07:38
Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • By wrapping a string path from resolveDiffPath in an array ([path]), setByPath now treats it as a literal key instead of a dotted path (unlike lodash.set); if any callers rely on dot-notation semantics from resolveDiffPath, this will change behavior and may need to be addressed either in resolveDiffPath or setByPath.
  • setByPath currently assumes obj is a non-null object and uses very loose any types; consider tightening the types (e.g. Record<string, unknown>) and early-returning or throwing when obj is null/non-object to avoid silent failures or runtime errors when misused.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- By wrapping a string `path` from `resolveDiffPath` in an array (`[path]`), `setByPath` now treats it as a literal key instead of a dotted path (unlike `lodash.set`); if any callers rely on dot-notation semantics from `resolveDiffPath`, this will change behavior and may need to be addressed either in `resolveDiffPath` or `setByPath`.
- `setByPath` currently assumes `obj` is a non-null object and uses very loose `any` types; consider tightening the types (e.g. `Record<string, unknown>`) and early-returning or throwing when `obj` is `null`/non-object to avoid silent failures or runtime errors when misused.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes i18n JSON diff processing so objects with numeric string keys (e.g. { "0": ... }) aren’t accidentally turned into arrays when building translation “extra” chunks.

Changes:

  • Added a custom setByPath utility to replace lodash.set and preserve numeric-string keys as object properties.
  • Updated diffJson to use setByPath when building the “added keys” object.
  • Added unit tests covering setByPath behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/lobe-i18n/src/utils/setByPath.ts New path-setter utility used to avoid lodash numeric-key-to-array behavior.
packages/lobe-i18n/src/utils/setByPath.test.ts Unit tests for the new setter, including numeric key scenarios.
packages/lobe-i18n/src/utils/diffJson.ts Swaps lodash.set for setByPath when constructing extra additions.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +23
// Create intermediate object if it doesn't exist or is not an object
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

setByPath currently stringifies every path segment and always creates {} for missing intermediates. This prevents creating real arrays when the path contains a numeric index (e.g. ['array', 3] from just-diff), so added array elements will be represented as objects with '3' keys instead of sparse arrays (behavior change vs previous lodash.set). Consider preserving arrays by creating [] when the next path segment is a number (array index), while still treating numeric strings (e.g. '0') as object keys to fix the reported issue.

Suggested change
// Create intermediate object if it doesn't exist or is not an object
if (!current[key] || typeof current[key] !== 'object') {
current[key] = {};
// Create intermediate container if it doesn't exist or is not an object.
// If the next path segment is a number, we create an array to preserve
// array semantics (e.g. ['items', 3]); otherwise we create an object.
if (
current[key] === undefined ||
current[key] === null ||
typeof current[key] !== 'object'
) {
const nextSegment = path[i + 1];
current[key] = typeof nextSegment === 'number' ? [] : {};

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +145
it('should handle number type in path (not just numeric strings)', () => {
const obj = {};
setByPath(obj, ['items', 0, 'name'], 'First');
setByPath(obj, ['items', 1, 'name'], 'Second');

expect(obj).toEqual({
items: {
'0': { name: 'First' },
'1': { name: 'Second' },
},
});

expect(Array.isArray((obj as any).items)).toBe(false);
});
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

The test should handle number type in path asserts that numeric number segments (0/1) are treated as object keys. In production, array diffs from just-diff use number indices (see existing diffJson tests), so encoding number segments as object properties makes it impossible for setByPath to preserve real arrays. Recommend adjusting this test (and adding a dedicated array case) so number path segments can represent array indices, while numeric string segments remain object keys.

Copilot uses AI. Check for mistakes.
Comment on lines 48 to +52
for (const item of add) {
const path = resolveDiffPath(entry, item.path as DiffPath, keyStyle);
set(extra, path, item.value);
// Use custom setByPath to preserve numeric string keys as object keys
const pathArray = Array.isArray(path) ? path : [path];
setByPath(extra, pathArray, item.value);
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

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

diffJson.diff now relies on setByPath to build extra for added paths, but there’s no integration coverage here for (1) numeric-string object keys staying objects (the reported bug) and (2) array index paths (e.g. ['array', 3]) producing the expected structure in result.entry. Adding/adjusting tests in diffJson.test.ts to assert result.entry for these cases would help prevent regressions in the translation chunk building pipeline.

Copilot uses AI. Check for mistakes.
@canisminor1990 canisminor1990 merged commit 9645ed8 into lobehub:master Feb 15, 2026
8 of 9 checks passed
@lobehubbot
Copy link
Member

❤️ Great PR @snomiao ❤️

The growth of project is inseparable from user feedback and contribution, thanks for your contribution!
项目的成长离不开用户反馈和贡献,感谢您的贡献!

github-actions bot pushed a commit that referenced this pull request Feb 15, 2026
### [Version&nbsp;1.26.1](https://github.com/lobehub/lobe-cli-toolbox/compare/@lobehub/[email protected]...@lobehub/[email protected])
<sup>Released on **2026-02-15**</sup>

#### 🐛 Bug Fixes

- **misc**: Preserve numeric string keys as objects instead of arrays.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Preserve numeric string keys as objects instead of arrays, closes [#164](#164) [Comfy-Org/ComfyUI_frontend#8718](Comfy-Org/ComfyUI_frontend#8718) ([9645ed8](9645ed8))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
@lobehubbot
Copy link
Member

🎉 This PR is included in version 1.26.1 🎉

The release is available on npm package (@latest dist-tag)

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants