okomarov / dash_on_flask

Dash on Flask with login_required (and application factory pattern)
MIT License
286 stars 126 forks source link

Add i18n usage example #11

Open merwok opened 4 years ago

merwok commented 4 years ago

Hello! I’m changing an existing Dash app for a covid-19 hackathon and am looking for ways to use flask i18n for the dash layout. Would it be in scope for this project to show how to do this?

okomarov commented 4 years ago

Hi I have not used Flask i18n but Miguel Grinberg's Mega tutorial covers that! Check out his Part XIII - i18n and l10n

merwok commented 4 years ago

Isn’t that about generic flask i18n, with JSON data and Jinja templates? The issue is that Dash components don’t look like regular views or responses, so the generic guides don’t help.

merwok commented 4 years ago

I have found a way to get flask-babel translations in dash JSON views, but it required 3 different parts + looking at plotly internals. Now we can define dash components like html.H1([_("app-title")]) and the correct language will be returned for the JSON request to /_dash-update-component.

Now the issue is that the method doesn’t work for plotly objects (e.g. a slider label for a plotly.express figure), so I’ll have to devise something else.

The takeaway is that plotly/dash seem to make no provision for internationalization of dashboards.

okomarov commented 4 years ago

Could you provide a reproducible example of the solution with the current issue?

Have you tried the localization for Plotly a suggested in changing locale with external CDN?

merwok commented 4 years ago

Sure, it’s from a public project, I’ll add a link when I create the PR.

Thanks for your link, it is about locales for built-in labels in plotly JS file, but my goal is to translate strings specific to one dashboard.

okomarov commented 4 years ago

Thanks for your link, it is about locales for built-in labels in plotly JS file, but my goal is to translate strings specific to one dashboard.

Then I misunderstood what you mean by:

Now the issue is that the method doesn’t work for plotly objects (e.g. a slider label for a plotly.express figure), so I’ll have to devise something else.

I guess it will make sense with the example and I am sure it would be a helpful addition for the community :)

merwok commented 4 years ago

I meant something like:

figure = px.choropleth_mapbox(
    dataframe, geojson=mtl_geojson,
    # skip many params
    labels={
        'date': _('my custom label'),
        'borough': _('Borough/city'),
    },
)
merwok commented 4 years ago

This branch has partially working translations: https://github.com/jeremymoreau/covid19mtl/pull/21

That project is a dashboard with 4 URLs: / serves French, /en English, /es Spanish and /zh Mandarin. Before the PR, 4 layouts have to be built for each page. With the PR, Flask-Babel is used to translate strings in Dash components.

The LazyString type is rejected by plotly validators (e.g. when doing fig.layout.sliders[0]['currentvalue']['prefix'] = _('date_slider_label'), which is the blocking issue I mentioned.

okomarov commented 4 years ago

I found a workaround, wrap the label in a tuple to bypass the string validation carried out by plotly: fig.layout.sliders[0]['currentvalue']['prefix'] = (_('date_slider_label'))

in _plotly_utils.py line 1020 the function validate_coerce does not support LazyString.

Several options:

  1. workaround with tuple
  2. clone plotly into third party, and add a check validate_coerce for LazyString which returns str(v)
  3. Raise issue with potly

Hope this helps!

merwok commented 4 years ago

I was indeed going to take option 3!

I tried the tuple idea (your example misses a comma to make a tuple btw) but the validation rejected it too (e.g. Invalid value of type 'builtins.tuple' received for the 'prefix' property of layout.slider.currentvalue […] The 'prefix' property is a string) or I got exceptions (TypeError: can only concatenate tuple (not "str") to tuple). Thanks for the suggestion nonetheless!

The only way I could satisfy these validators would be to make LazyString a subclass of str, then do a lot of work to intercept add, radd, mod, etc. to keep the string lazy (and that would not work if the lazy string ends up in a list passed to str.join), then somehow hook just before the figure is serialized and sent to the browser.

merwok commented 4 years ago

Issue here: https://github.com/plotly/plotly.py/issues/2475