-
Notifications
You must be signed in to change notification settings - Fork 964
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
base: main
Are you sure you want to change the base?
Conversation
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 |
I'm not sure I understand this. I found a way to use the server-side rendering of translations because:
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 Please let me know if I've misunderstood what you're asking. |
I managed to get the plurals extracted. It looks like python-babel expects particular function names. TODO:
Question:
|
Provide two functions for gettext so IDe highlighting is useful.
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.
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: |
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. |
Test errors:
This one could be fixed by adding - msg += " " + " ".join(results["feedback"]["suggestions"])
+ msg = str(msg) + " " + " ".join(results["feedback"]["suggestions"])
I'm not sure how to fix this error. Other form tests seem to be able to set |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took one last look and I have one outstanding issue here before we can merge.
This removes a step and allows translation updates (static_pipeline) to generate the js bundles. The messages.json files are not required when translation data is bundled like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good - I did some basic testing and it looks like it creates a full JS bundle per language, so once a local has changed, the only JS resource loaded is the one for that locale.
I put a bunch of comments inline - let me know if they don't make sense.
I'm also a little concerned about a JS injection attack from externally-supplied values - is there something I'm overlooking on how these are sanitized?
webpack.plugin.localize.js
Outdated
const denied = new RegExp( | ||
"[~`^$_\\[\\]{}\\\\'\"\\.#@\\f\\n\\r\\t\\v\\u00a0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]+"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: add a comment to denote where this regex originated, and what it is denying
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in 8e2e3f5
compressionOptions: { level: 9, memLevel: 9 }, | ||
compressionOptions: {level: 9, memLevel: 9}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: does our linter not check and enforce these kinds of changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(might be?) addressed in 7d1e8cb
const data = { | ||
"": {"language": "fr", "plural-forms": "nplurals=2; plural=n > 1;"}, | ||
"My default message.": "My translated message.", | ||
"My %2 message with placeholders %1.": "My translated %1 message with placeholders %2", | ||
"My message with plurals": ["My translated message 1.", "My translated messages 2."], | ||
"My message with plurals %1 again": ["My translated message 1 %1.", "My translated message 2 %1"], | ||
}; | ||
const pluralForms = function (n) { | ||
let nplurals, plural; | ||
nplurals = 2; plural = n > 1; | ||
return {total: nplurals, index: ((nplurals > 1 && plural === true) ? 1 : (plural ? plural : 0))}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: this looks a lot like a parametrized test, is that the case here? If so, would it make more sense to use jest's .each
to express the individual tests to reduce the amount of logic baked in to the test case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in b6fc1e6
import timeAgo from "../../warehouse/static/js/warehouse/utils/timeago"; | ||
import {delay} from "./utils"; | ||
|
||
describe("time ago util", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yay, we have tests now!
} | ||
|
||
export function ngettextCustom(singular, plural, num, extras, data, pluralForms) { | ||
// this function allows for testing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: is this comment still accurate, if ngettext
is basically a wrapper for this, with some defaults?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in d32b447
var messagesAccessLocaleData = {"": {"language": "en", "plural-forms": "nplurals = 2; plural = (n != 1)"}}; | ||
|
||
// the value for 'messagesAccessPluralFormFunction' is set by webpack.plugin.localize.js | ||
// default is en | ||
var messagesAccessPluralFormFunction = function (n) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: should these be var
or const/let
? (again, I wonder what's happening with the linter...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in d32b447
webpack.config.js
Outdated
return file; | ||
}; | ||
// Refs: https://github.com/shellscape/webpack-manifest-plugin/issues/229#issuecomment-737617994 | ||
// publicPath: "", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: why is this previously-set value commented out? If it's no longer valid, let's remove it, along with the comment above as to why it's there, or move it to where it's now being set, or extract it to a constant that can be easier commented and reused.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed in 0f13ec1
webpack.plugin.localize.js
Outdated
if (refs.every(refLine => !refLine.includes(".js:"))) { | ||
continue; | ||
} | ||
result[value.msgid] = value.msgstr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to sanitize the user input at this point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(partly?) addressed in fd0dd80
Using ngettextCustom makes it possible to test the functions and use the function signatured required by pybabel. Fix eslint issues.
This makes it easier to see the test cases. Add comments to make it clearer what the data is used for.
Added comment to explain what the pattern is checking.
This is important as these values will be embedded into the messages-access.js file.
…ture/translation-js
…ture/translation-js
Co-authored-by: Dustin Ingram <[email protected]>
Co-authored-by: Dustin Ingram <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
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:
These are the benefits of this approach:
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:
Considerations:
Notes
Relates to issues: