localized-strings
localized-strings copied to clipboard
More robust support of objects in localization defs
Hi Stefano and thanks for this library,
Object literals are currently supported and mentioned in the doc, for instance:
en:{
how:"How do you want your egg today?",
boiledEgg:"Boiled egg",
softBoiledEgg:"Soft-boiled egg",
choice:"How to choose the egg",
fridge: {
egg: "Egg",
milk: "Milk",
}
},
However, it can be useful to include more complex objects (non literal) in these definition, a use case being to include React components (see this issue in react-localization) but this is currently impossible because localized-strings tries to recursively analyze object properties to compute its fallback rather than just keeping the same object.
The culprit is this snippet:
/**
* Load fallback values for missing translations
* @param {*} defaultStrings
* @param {*} strings
*/
_fallbackValues(defaultStrings, strings) {
Object.keys(defaultStrings).forEach(key => {
if (
Object.prototype.hasOwnProperty.call(defaultStrings, key) &&
!strings[key] &&
strings[key] !== ""
) {
strings[key] = defaultStrings[key]; // eslint-disable-line no-param-reassign
if (this._opts.logsEnabled) {
console.log(
`🚧 👷 key '${key}' not found in localizedStrings for language ${this._language} 🚧`
);
}
} else if (typeof strings[key] !== "string") {
// It's an object
this._fallbackValues(defaultStrings[key], strings[key]);
}
});
}
In this code, for each key there are three options:
- if the key is missing the corresponding definition for the default language is copied
- if this key is found and the definition is not a string it is considered as an object and the same function is applied to its properties
- otherwise the definition is copied
The issue is thus that the test for the second option should differentiate simple (or literal) objects from anything else.
As a quick and dirty hack, I have added a simple test to exclude React components identified by their $$typeof
property:
}
} else if (typeof strings[key] !== "string" && defaultStrings[key].$$typeof === undefined) {
// It's an object (and it's not a React component)
_this4._fallbackValues(defaultStrings[key], strings[key]);
}
This is working fine for me but a more generic test would be preferable.
Differentiating "simple" and "complex" objects in JavaScript isn't obvious but there are proposals which should be more robust than just checking that it's not a string, see for instance:
- This answer by Jovie Fox on devasking.com.
- The implementation of lodash isPlainObject.
If you think that this is a useful feature I can test these options and propose a fix!
Hi Eric, thanks for your contribution! You're right, localized-strings should be framework agnostic... I don't see a use for this implementation in the main library, but the check could be placed in the react specific version maybe with the hack you proposed or a more generic solution... You could provide a test and a PR in that repository. What do you think?
Actually, even if the issue appears in React because of the way its components behave, I think that it's not a good idea to duplicate properties from an object into another one if these objects are not simple/literals objects (these objects might even be of different types and copying properties without using their methods does seem very unsafe).
The more I think about it, the more I think that checking that we only do this when we have simple/literals is a useful sanity check in general and not only for React applications.
That being said, to be honest, I have forked this repo and I am having a hard time setting up a Jasmine test to isolate the problem.
BTW, I have found that the current version is working fine with JSX nodes and jsdom on nodejs and that users of this library should be able to use it with JSX out of the context of React (that would be a solution to the issues about breaking long lines).
Hi Stefano,
Just noticed some more things:
-
The original strings object is mutated when a language is set. I don't think it's a problem right now but this is potentially dangerous.
-
You can use arrays as well as objects:
anArray: ['eccellente', 'buono'],
The fallback mechanism will then add missing array members if they are missing using array indexes as object keys (which can be dangerous and should probably be documented).
- It is possible to "isolate" objects and stop fallback recursion by enclosing them in functions:
plainObjectInFunction: () => ({
a: 'aaa2',
b: 'bbb2',
}),
(and then, of course you have to invoke the function to get their value: strings.plainObjectInFunction()
).