-
Notifications
You must be signed in to change notification settings - Fork 602
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
Implement app example using webpack #481
Conversation
The real-world use case. I'm migrating a large code base to a set of modern tools, including webpack and, probably, Globalize for i18n, but my messages are in PHP (and that cannot be changed during this migration because they are reused by the backend i18n system). We're currently developing a loader that provides the messages to our frontend i18n system in the required format, and it would fit well if we decide to replace our frontend i18n with Globalize.
The real-world use case is obvious: language switching without page reload. |
I see. Can you provide examples of the not-just-from-plain-JSON-alternatives please?
👍 |
@rxaviers I'll give you an example but please do not write a specific loader for my specific example. A generic ability to specify a loader (or maybe better a plain function which could delegate to any other code if needed) would be better. Our i18n system is Yii-based and uses a notion of per-module "category" and "key" (not just "key" which Globalize uses AFAIK), so a transform is required. But the system is highly customized to support multi-level overrides and fallbacks for each language, so this should be somehow handled by our custom loader (we actually delegate to the PHP component which knows how to do overrides and fallbacks, it does this for the server-side). Here's an example of a single message file (of a single "category" for a "dialogs" module): <?php
return [
"_meta" => [
"selector" => [
"en",
],
],
"dlg.message.title" => "Message",
"dlg.message.button.close" => "Close",
"dlg.error.title" => "Error",
"dlg.info.title" => "Info",
"dlg.confirm.title" => "Confirmation",
"dlg.confirm.button.yes" => "Yes",
"dlg.confirm.button.no" => "No",
"dlg.prompt.title" => "Input",
"dlg.prompt.button.yes" => "OK",
"dlg.prompt.button.no" => "Cancel",
]; It's time for one more unexpressed thought that I've written about before. We need to be able to load different sets of messages for different apps based on a single platform (each app has one or more webpack entry points). Imagine two different applications with a set of common modules, each module depends on its own "category" with translations, each translation may be overridden at the application level. We should be able to build these apps separately and bundle only the translations being required by each app's components. |
Sure, the messages option can be made to either accept the string (as documented above) or a function that passes new globalizePlugin({
production: options.production, // true: production, false: development
developmentLocale: "en", // locale to be used for development.
supportedLocales: [ "en", "es", "zh" ], // locales that should be built support for.
messages: function( locale ) {
// Messages in the JSON format for requested locale.
return getMessagesFor( locale );
}
output: "globalize-compiled-[locale].[hash].js" // build output.
}); Will include that...
The current implementation (rxaviers/globalize-webpack-plugin) is smart to bundle the translations being required only. |
Wow, that's pretty cool if I get you correct! How would you require specific message files from component files then, not having a gigantic "messages" object? |
Does it by chance support asynchronous loading e.g. via a Promise returned from the "messages" function? |
Making myself clear, the current implementation requires a gigantic "messages" object to be available for the webpack plugin. But, the plugin is smart to use the messages being required only (and therefore, to generate the final bundle with the translations being required only).
Async might be tricky in webpack. Most of the compilation plugins (for example, [1], [2]) are sync calls. I'd like to understand how you handle your custom messages and where async comes in. For example, are you handling files locally or remote? Does the processing require async? We could chat on IRC (http://irc.jquery.org/, #globalize).
It's possible to pass, as another argument, the filename of the JS file being handled (in webpack terminology, the // For example:
messages: function( locale, request ) {
// Messages in the JSON format for requested locale.
// locale: E.g. "en", "en-GB", etc.
// request: E.g., "my-app/index.js", "my-app/components/foo.js", so your function
// could look into "my-app/index.messages.json" or
// "my-app/components/foo.messages.json" for example
return getMessagesFor( locale, request );
} Did I answer your question? |
The current implementation executes an external process (a PHP console command which composes the messages using our custom override logic and emits them to stdout) which is an async operation. I'm sorry for confusing you, we are currently using a plugin, not a loader (we tried loader before but failed). Because of lack of documentation on webpack plugin features we are trying to learn from existing plugins (including yours from now). The current implementation looks hacky and requires us to include the i18n bundle separately, not using webpack autoloading system. The code is not public, but I can show you some generic pieces we came up with. We use the compiler.plugin('normal-module-factory', function (nmf) {
langCategories = [];
nmf.plugin('before-resolve', function (data, beforeResolveCallback) {
// ...
langCategories.push(langCategory);
data.request = __dirname + '/.i18n-dummy'; Then we use the compiler.plugin('emit', function (compilation, compilationCallback) {
// ...
compilation.assets['some-prefix-' + language + '.js'] = source;
//
Yes, this would be better, I think. This makes the "messages" function return a per-request object instead of the gigantic one, and Globalize then merges all the messages together, right? |
Webpack is very powerful, but it has been really laborious to develop the plugin. I've also been using another plugins as a baseline given the lack of doc, plus I have also been reading the source code and have counted with help from bebraw and sokra. Thanks for sharing your excerpts. I understood that your
Ok, will include it or suggest something else after thinking more about it. |
Yes. That's the hacky flavor this solution gets. We access the translations via a singleton component which is populated with the messages. We have to side-load these files by a plain script tag or our own script loader, not by the webpack auto-loader. The messages get injected via a global variable shared between the core JS and the messages JS. It looks like this (the code is pseudo-code): // some-prefix-en.js
window.globalI18nManager.addMessages("some-category", {
"some-key": "Some message"
}, "en");
// bundle.js
window.globalI18nManager.setLanguage("en");
$someElement.text( window.globalI18nManager.formatMessage("some-category", "some-key") ); |
7fc75f8
to
0ddb68f
Compare
0ddb68f
to
a5cddc2
Compare
- Change demo. - Inlcude pt messages.
{% | ||
} | ||
} | ||
%} |
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.
It generates stuff like:
<script src="vendor.07c80e6dfcd89796c7d5.js"></script>
<!--
Load support for the `en` (English) locale.
For displaying the application in a different locale, replace `en` with
whatever other desired supported locale, e.g., `pt` (Portuguese).
For supporting additional locales simultaneously and then having your
application to change display dynamically, load the multiple files here.
Then, use `Globalize.locale( <locale> )` in your application to dynamically
set it.
-->
<!-- <script src="i18n/zh.07c80e6dfcd89796c7d5.js"></script> -->
<!-- <script src="i18n/pt.07c80e6dfcd89796c7d5.js"></script> -->
<!-- <script src="i18n/es.07c80e6dfcd89796c7d5.js"></script> -->
<script src="i18n/en.07c80e6dfcd89796c7d5.js"></script>
<script src="app.07c80e6dfcd89796c7d5.js"></script>
@sompylasar your usecase should be tracked by rxaviers/globalize-webpack-plugin#1 and rxaviers/globalize-webpack-plugin#2 |
Content team, @kswedberg, @arthurvr, if anyone has any spare time, it would be great to count with your continued help reviewing this PR as well. Specially in: |
@rxaviers Thanks! |
thanks, @rxaviers . I should be able to do a quick review this evening. |
@@ -0,0 +1,18 @@ | |||
{ | |||
"name": "globalize-full-app-npm-webpack", |
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.
Dunno how we generally do that, but if a package is marked private
giving a name
is kinda moot.
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.
👍
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.
Thank you all |
Includes Simplified Chinese. Ref #481
"relative-time-label": "Tiempo Relativo", | ||
"message-1": "Un ejemplo de mensaje usando números mixtos \"{number}\", monedas \"{currency}\", fechas \"{date}\", y tiempo relativo \"{relativeTime}\".", | ||
"message-2": [ | ||
"Un ejemplo de mensaje con soporte de pluralizaciónt:", |
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.
Small typo, extra t
at the end of pluralizació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.
Thanks, d317164.
I wonder what is the correct place to report the issues with Feels that globalize compiler is trying to be too smart for its own good (too much optimisation?): Methods like Also, evaluation may consider variables undefined, while they're not: // Angular translation filter
angular.module('app')
.filter('t', function($filter) {
return function (translationKey) {
return Globalize.formatMessage(translationKey);
};
}); // Compilation error
./node_modules/globalize-webpack-plugin/GlobalizeCompilerHelper.js:72
throw e;
^
ReferenceError: translationKey is not defined
at eval (eval at extractor (./node_modules/globalize-compiler/lib/extract.js:67:9), <anonymous>:3:36)
at ./node_modules/globalize-compiler/lib/compile-extracts.js:54:23
at Array.reduce (native) If I change the translationKey into valid Any wise words to help compiler to make it work like expected? |
Thanks to @unindented. Amends a6c8a6e Ref #481 Ref #490
Hi @juhamust, The problem you're facing is because you're using a dynamic construction in your Globalize options and the compiler is static. It doesn't mean you cannot use dynamic patterns in your code, but you have to change your code like the below: var availableMessageFormatters = {
foo: Globalize.messageFormatter(<foo>),
bar: Globalize.messageFormatter(<bar>)
...
};
// Angular translation filter
angular.module('app')
.filter('t', function($filter) {
return function (translationKey) {
return availableMessageFormatters[translationKey];
};
}); Note this is actually a good constrain for your code. Because, you could potentially run into a case where you are trying to format a message in production that you have never provided translation for. Did I answer to your question? Would you change anything in our docs to make this clear? PS: Anyway, if you need to report errors to |
@rxaviers thanks a lot for your prompt and informative response. That really took me surprised, so perhaps the documentation really could be improved about the topic. Duh. I tried building multilingual lookup table, but it still has that dynamic translationKey variable there. // Build translation message lookup table
var messageLookupTable = {};
var messages = require('json!../common/locales/messages/en.json');
_.each(['fi', 'en', 'es', 'de'], function(languageCode) {
messageLookupTable[languageCode] = {};
// Using english translation keys as a reference
_.each(messages['en'], function(msg, translationKey) {
messageLookupTable[languageCode][translationKey] = Globalize.messageFormatter(translationKey);
});
}); So I really need to generate JS versions from all of those JSON message files? That's something I would expect the compiler is doing on my behalf? Thanks again. UPDATE: I created a simple from JSON to JS conversion step in building phase and it seems to work as expected. |
Thanks to @AhmedMustafa. Amends a19869f Ref #481
@juhamust, can you give a little more details on what you are trying to accomplish? Is this part of a frontend or backend application? Where are you going to use |
@rxaviers I responded in separate ticket instead of polluting this PR thread |
👍 I've replied you there. |
WIP fix for #464
Requires:
ToDo:
npm run start
in this example)npm run build
in this example)en
in this example).en
,es
andzh
in this example).About this implementation:
https://github.com/jquery/globalize/blob/fix-464-webpack-example/examples/app-npm-webpack/webpack-config.js#L38-L44
production is a boolean that tells the plugin whether it's on production mode (i.e., to build the precompiled globalize data) or not (i.e., to be in development mode - will use Live AutoReload HRM).
developmentLocale tells the plugin which locale to automatically load CLDR for and have it set as default locale for Globalize (i.e.,
Globalize.locale(developmentLocale)
).supportedLocales tells the plugin which locales to build/produce compiled-globalize-data for.
output is the name scheme of the built files.
Development mode
Start the Live AutoReload server by running the command below.
Then, point your browser to
http://localhost:8080/
.Production mode
Build the production bundles by running the command below.
The build files are placed in
dist
. Point your browser at./dist/index.html
to check.