Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow translating from javascript files #15612

Merged
merged 73 commits into from
Jan 16, 2025

Conversation

cofiem
Copy link
Contributor

@cofiem cofiem commented Mar 17, 2024

This PR enables translating strings in javascript.

It demonstrates the js translation in use by applying it to the timeago.js js util.

Edit: added overview of the two mutually-exclusive approaches.

Approach: webpack plugin the generates js message bundles

This is the current state of this PR.

I attempted an approach similar to https://www.npmjs.com/package/@zainulbr/i18n-webpack-plugin
i.e. embed the translations into separate js bundles, one for each language.

Unfortunately, the existing webpack plugins were not suitable (issues with webpack 5 and/or did not work for the warehouse setup).
However, I did manage to create a webpack plugin that does the job (in webpack.plugin.localize.js).
The plugin checks the plural forms and embeds them into the generated js bundles.

So, the development process with these changes:

  • Use gettext or ngettext as appropriate in js files
  • Run make translations: pybabel extracts messages from js files, then a python script generates messages.json files for only the KNOWN_LOCALES and only includes translations that are needed in js
  • Run make static_pipeline: the webpack plugin embeds the translated data from the messages.json files
  • At runtime, the matching js bundle for the current locale is set in base.html, and gettext.js is initialised with the translation data. It can determine plurals and do string templating.

These are the benefits of this approach:

  • Uses the existing translation and js tooling - make, pybabel, locale files, webpack
  • Enables plurals and template placeholders
  • Limits or does not use the hard to cache routes nor additional http requests
  • Limits the number of unmaintained packages required, and works with webpack 5
  • Straightforward to maintain and use as part of contributions

Approach: request from client to server to get translation for a given message code

A js util fetch-gettext.js that requests the translation code returns a promise, which contains the translation text from the server, using the locale set on the server-side.

view the changes for this approach.

Changes:

  • enables extracting strings from javascript using pybabel,
  • adds a route that renders the appropriate translation using the existing machinery, and
  • adds js util for obtaining the translated string.

Considerations:

  • may make many http requests for translations

Notes

  • Pybabel seems to require particular js function names and the config to change them does not work, similar to this pybabel issue. This is ok, I've used the names expected by pybabel.

Relates to issues:

@di
Copy link
Member

di commented Mar 19, 2024

Very cool! I like this approach a lot, I think this will definitely work.

One thing I'm wondering: since we probably won't have a ton of JS translations, would it make sense to load them all for a given locale in a bundle via a single request, as opposed to making N different requests?

@cofiem
Copy link
Contributor Author

cofiem commented Mar 19, 2024

One thing I'm wondering: since we probably won't have a ton of JS translations, would it make sense to load them all for a given locale in a bundle via a single request, as opposed to making N different requests?

I'm not sure I understand this.

I found a way to use the server-side rendering of translations because:

  • I can't see that there's a way to get just the js translation strings to make a bundle of translation strings
  • each translation can have up to 6 variants, which are chosen based on the num parameter
  • some translations have placeholders in the string, and I didn't want to have to find or make a way to fill the placeholders in js

If you're concerned about the server load or frequency of requests, the browser cache seems to work well, and there's likely some additional caching to be done in the server-side view that could reduce the need to execute the localiser.pluralize or localizer.translate.

Please let me know if I've misunderstood what you're asking.

@cofiem
Copy link
Contributor Author

cofiem commented Mar 19, 2024

I managed to get the plurals extracted.

It looks like python-babel expects particular function names.

TODO:

  • I'll include some screenshots of the js translations in action.
  • I may also have time to go through and make the rest of the text that's been identified in the other issues as translatable.
  • fix the couple of failing tests

Question:

  • Is there an existing way to cache the output of a view that depends on the request.params? That would work well for caching the translation view on the server side.
    Maybe origin_cache?

@di
Copy link
Member

di commented Mar 20, 2024

If you're concerned about the server load or frequency of requests, the browser cache seems to work well, and there's likely some additional caching to be done in the server-side view that could reduce the need to execute the localiser.pluralize or localizer.translate.

Yes, I was thinking about balancing the number of requests with the size of the requests. It's probably an over-optimization though, our CDN will cache these responses by default so it's not the load on our backend that I was concerned with, just the number of requests the user's browser would have to make after the page has loaded. I think it's fine to move forward as-is, just wanted to think through it a bit.

Is there an existing way to cache the output of a view that depends on the request.params? That would work well for caching the translation view on the server side.

By default, we cache all requests, but strip all URL parameters when caching a given view (with some exceptions). We can add an exception for this view. This happens here:

/~https://github.com/pypi/infra/blob/9568346c6781a6d9e754e9edc3883dd34febd97b/terraform/warehouse/vcl/main.vcl#L103-L126

@cofiem
Copy link
Contributor Author

cofiem commented Mar 24, 2024

The only remaining question is what @di was asking - is the number of requests made by the browser a problem?

Some example screenshots of the js translations in use.

Screenshot 2024-03-24 181338
Screenshot 2024-03-24 181556
Screenshot 2024-03-24 181952
Screenshot 2024-03-24 182303

@cofiem
Copy link
Contributor Author

cofiem commented Mar 24, 2024

Test errors:

> msg += " " + " ".join(results["feedback"]["suggestions"])
E TypeError: unsupported operand type(s) for +=: 'LazyString' and 'str'

This one could be fixed by adding str(msg), but there might be a better way?

- msg += " " + " ".join(results["feedback"]["suggestions"])
+ msg = str(msg) + " " + " ".join(results["feedback"]["suggestions"])

> return request.localizer.translate(_translation_factory(message, **kwargs))
E AttributeError: 'NoneType' object has no attribute 'localizer'

I'm not sure how to fix this error. Other form tests seem to be able to set request=pretend.stub(), but the Validator directly uses from warehouse.i18n import localize as _, which errors at request.localizer.

@cofiem cofiem marked this pull request as ready for review March 24, 2024 10:40
Makefile Outdated Show resolved Hide resolved
webpack.plugin.localize.js Outdated Show resolved Hide resolved
cofiem and others added 4 commits August 22, 2024 19:13
Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
Co-authored-by: Dustin Ingram <di@users.noreply.github.com>
Copy link
Member

@di di left a comment

Choose a reason for hiding this comment

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

LGTM

@di di requested a review from miketheman September 23, 2024 15:36
Signed-off-by: Mike Fiedler <miketheman@gmail.com>
Signed-off-by: Mike Fiedler <miketheman@gmail.com>
Signed-off-by: Mike Fiedler <miketheman@gmail.com>
Copy link
Member

@miketheman miketheman left a comment

Choose a reason for hiding this comment

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

I'm +1 to these changes, and will add a few commits that fix minor nits that have crept in since. I plan to merge tomorrow morning.

@miketheman miketheman merged commit ba6d7bd into pypi:main Jan 16, 2025
20 checks passed
@di
Copy link
Member

di commented Jan 16, 2025

Looks like this is causing build failures: /~https://github.com/pypi/warehouse/actions/runs/12811292443/job/35720280486?pr=17373

@di
Copy link
Member

di commented Jan 16, 2025

I think this should resolve it: #17424

@cofiem cofiem deleted the feature/translation-js branch January 17, 2025 00:55
miketheman added a commit to miketheman/warehouse that referenced this pull request Jan 17, 2025
    removed 28 packages, and audited 1020 packages in 904ms

Final direct usage removed in pypi#15612

Signed-off-by: Mike Fiedler <miketheman@gmail.com>
ewdurbin added a commit that referenced this pull request Jan 18, 2025
* chore(ci): update node version

Sync with `Dockerfile`

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): npm update

    added 60 packages, removed 171 packages, changed 233 packages, and audited 1082 packages in 22s

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): update webpack-cli

    changed 7 packages, and audited 1082 packages in 1s

Refs: /~https://github.com/webpack/webpack-cli/blob/5a6a70b8dfe38e147ee158018dd154e49a5a0c0d/CHANGELOG.md#601-2024-12-20

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): update stylelint-config-standard-scss

    changed 1 package, and audited 1082 packages in 965ms

Refs: /~https://github.com/stylelint-scss/stylelint-config-standard-scss/blob/d0feabca7e43ddcb033f0636ccdeb4ca9b0671ad/CHANGELOG.md#1400

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): update gettext-parser

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): update sharp

    added 2 packages, removed 38 packages, changed 1 package, and audited 1046 packages in 1s

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): remove glob

    removed 28 packages, and audited 1020 packages in 904ms

Final direct usage removed in #15612

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

* chore(deps): update sass-loader

Needs an update to the "local" loading path.
Refs: https://webpack.js.org/loaders/sass-loader/#resolving-import-and-use-at-rules

Signed-off-by: Mike Fiedler <miketheman@gmail.com>

---------

Signed-off-by: Mike Fiedler <miketheman@gmail.com>
Co-authored-by: Ee Durbin <ewdurbin@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
i18n Internationalization javascript requires change to JavaScript files
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants