typesafe-i18n icon indicating copy to clipboard operation
typesafe-i18n copied to clipboard

Using `extendDictionary` does not work well with nested namespaces.

Open apatrida opened this issue 1 year ago • 3 comments

Version

5.26.2

Describe the bug

If you extend a dictionary (extendDictionary) and set a value in a nested namespace, it erases that whole namespace instead of just extending the one value. Details represented by the tests below.

It is unlikely you ever intend to erase values when extending a dictionary, you typically only override. So the default behavior is counter to that.

You can explicitly extend each nested namespace by hand extending, but only if you ignore the generated extendDictionary function that "hides" the original, and then use the original.

Reproduction

Taking the test cases for this utility method and adding this test case, it fails:

	simple: 'Hello',
	nested: { value: 'Hello nested' },
	nested2: { value1: "One value", value2: "Another value", value3: "And yet another value"},
	deeper: { nesting: { value1: "v1", value2: "v2" }}
}

test('nested extend with more complex', () => {
	const extended = extendDictionary(translationComplex, {
		simple: 'Hello extended',
		nested2: { value2: 'Another value extended' },
		deeper: { nesting: { value2: 'v2 extended'}}
	})
	assert.equal(extended, {
		simple: 'Hello extended',
		nested: { value: 'Hello nested' },
		nested2: { value1: "One value", value2: "Another value extended", value3: "And yet another value"},
		deeper: { nesting: { value1: "v1", value2: "v2 extended" } }
	});
})

You can actually do this to pass:


test('nested extend explicit at each level with more complex', () => {
	const extended = extendDictionary(translationComplex, {
		simple: 'Hello extended',
		nested2: extendDictionary(translationComplex.nested2, { value2: 'Another value extended' }),
		deeper: extendDictionary(translationComplex.deeper, { nesting:
				extendDictionary(translationComplex.deeper.nesting, { value2: 'v2 extended'})
		})
	})
	assert.equal(extended, {
		simple: 'Hello extended',
		nested: { value: 'Hello nested' },
		nested2: { value1: "One value", value2: "Another value extended", value3: "And yet another value"},
		deeper: { nesting: { value1: "v1", value2: "v2 extended" } }
	});
})

but this is really awkward. And is more so because the symbol extendDictionary is replaced in local generated code meaning you have to ignore that, and import the original version to make it work.

The original tests by @osdiab were not sufficient to be clear what was intended here.

Logs

No response

Config

No response

Additional information

No response

apatrida avatar Sep 14 '23 06:09 apatrida

The fix is to change:

extend({}, base, part) as Translation

to:

extend(true, {}, base, part) as Translation

apatrida avatar Sep 14 '23 07:09 apatrida

workaround is to add your own utility function:

export const initExtendLanguage =
  <TranslationType extends BaseTranslation>() =>
    <Base extends BaseTranslation | TranslationType, Translation extends Base>(
      base: Base,
      part: DeepPartial<ToGenericString<Translation>>,
    ): Translation =>
      extend(true, {}, base, part) as Translation

export const extendLanguage = initExtendLanguage()

and pnpm install [email protected] to use the same utility class already in the base library.

apatrida avatar Sep 14 '23 07:09 apatrida

I fix it by use deep-extend library

StagnantIce avatar Jun 20 '24 10:06 StagnantIce