Some time ago when I got tired of manual creation of translations via callbacks, it hit me that there must be an easier way.
Therefore I started testing and found an interesting solution but I need your feedback on some of the aspects. It all works and translates dynamic content. Works like a charm even with dmc notifications.
My approach bases on mutation observer and tree walker which is good but not ideal, as sometimes the number of mutations is immense. This is especially visible when hovering over translated points on chart or wrapped custom components that change state (like DevExtreme PivotGrid).
I am not great front end specialist, therefore I humbly ask for advice:
mutation observer, how to limit it from reacting to the same elements over and over (caching results is not the solution)
is this approach robust?
are there any other elements that should be considered?
As for now I am in a dead end due to probable performance issues.
As you said, relying on a mutation observer is maybe overkill.
I don’t know how you could optimize this way, but I can tell you how I did to translate my app.
I decided to implement it “the Dash way” with callbacks.
I have a dcc.Store("language_memory") that stores the current language.
I load a list of translation keys in JSON like you did
I fetch all elements in my layout that ends with “_text”. E.g. dropdown_category_text. Others are not translated.
I dynamically create a callback for each layout element with Input("language_memory") and Output(“element”, “children”). Or for a dropdown, it will be “options”. I implemented 4-5 specific cases like this.
That’s it.
So basically, each time that I change the language memory, that updates (without page reload) all elements text.
Now, this does not work with dynamically added content. For example, if your plotly chart is created dynamically via a callback, then I handle the translation in this callback and add language_memory as input again.
I think that anyway you don’t need a full live translation. Users don’t often change their language, this should be done at loading time as much as possible, IMO.
Hi @spriteware , thank you for the answer - I am using the same approach as you described in my prod apps, but it’s painful and drains my energy as I work full time with dash. When you take couple of devs that have to focus on additional translation callbacks it hurts team productivity, this is also not a robust approach imo.
Language change via the dropdown is just an example, initial language value is fetched from navigator then when change appears, the persistance is utilized and after refresh user has his language of choosing (in this demo it refreshes page after dropdown change- as you said there’s no need for this).
What I hope to get, is something similar to i18n for example, where all I care about is a tag, that I have translation for.
I tested approach with mutation observer in bigger apps and did not see significant performance issues when used only with compatible objects (no dynamic DOM). For now the best what I came with was something in between, translation header function for agGrid, figure creation with translate function inside and mutation observer for everything static.
Can you give an example of what you call “additional translation callbacks” ? That way we can discuss about the differences, because I don’t feel the pain you are talking about.
In the solution that I gave you, you don’t need to write more callbacks. You just need to update a translation file when new translations are required.
Here is a pseudo code to be clear:
text_components = list_textual_components(app.layout) # get all "xxxx_text" elements
text_components_ids = [cid for cid, prop, options in text_components]
logging.debug(f"Detected components for translation: {str(text_components_ids)}")
@app.callback(
[Output(cid, prop) for cid, prop, options in text_components],
Input("language_memory", "data"),
)
def translation_callback(language):
if language is None:
print("Language is None -----> PreventUpdate")
raise PreventUpdate
translated_content = []
for cid, prop, options in text_components:
# in case the id is a dictionary, use its values to build a string id
if type(cid) == dict:
cid = "_".join([k for k in cid.values()])
# we either update the component itself or some properties: placeholder, label, options
if prop == "children":
value = translate(language, cid)
elif prop == "placeholder":
value = translate(language, cid + ".placeholder")
elif prop == "label":
value = translate(language, cid + ".label")
elif prop == "options":
value = []
for option_dict in options:
value.append(
{
"label": translate(language, cid + f".{option_dict['value']}.label"),
"value": option_dict["value"],
}
)
else:
raise ValueError("Incorrect property to update")
translated_content.append(value)
return translated_content
It’s one callback that translates the entire app.
The translate() function is the one accessing to the transaltion file and is used like translate("en", "some_key_in_json")
So, a mutation observer would be able to catch most things, even for a component internally adjusting its props. The callback method wouldn’t be able to handle this directly, and then you would need to write additional callbacks to handle the native functions of hoverData, etc.
All dash components use a dash renderer function called setProps, it could be possible to utilize this function in order to insert a translation from the original text to the target translation.
I thought there was something on the road map for translations, and this would actually be something that I am interested in.
@adamschroeder, do you know of anything currently in the work for this? I thought there were some topics on this as well?
@spriteware yes, I misunderstood your approach at first, but the pseudocode you shared cleared it up.
This is very helpful, but i believe there must be a way for cleaner solution, because it:
requires id for every translation (i think usage of attribute would be cleaner, but creates own set of problems)
translates whole element that is referenced (logic can be adjusted to do differently)
does not work with dynamically generated content as the register is created on initial load
troublesome with dash initial load (layout with lambda)
I personally like to keep number of ids to minimum, only the functional ones. I will play with this, if something interesting comes out, will surely share.
Alright guys, thank you for the discussion. In the end I was able to implement i18nextify. This is something in between, it bases on i18next but adds nicely tuned mutation observer, which actually stops the flickering.
I updated the repo with example, so if you wanna check it out - go ahead repo dash-i18n