Web-friendly locale loading?
Closed this issue · 6 comments
Thanks for this library! Do you have any advice on how to use this library, along with locales, in a browser without needing to transpile the code myself from CommonJS? You are shipping a UMD (bundle/relative-time-format.min.js
) so I can load the library, but the locales are only provided as CommonJS.
Here’s what I’m currently using to load (using AMD require
):
RelativeTimeFormat = {
if (Intl.RelativeTimeFormat) return Intl.RelativeTimeFormat;
const module = window.module = {};
const [{default: RelativeTimeFormat}, long, short, narrow, quantify] = await Promise.all([
require("relative-time-format@0.1/bundle/relative-time-format.min.js"),
require.resolve("relative-time-format@0.1/locale/en/long.json").then(fetch).then(r => r.json()),
require.resolve("relative-time-format@0.1/locale/en/short.json").then(fetch).then(r => r.json()),
require.resolve("relative-time-format@0.1/locale/en/narrow.json").then(fetch).then(r => r.json()),
require("relative-time-format@0.1/locale/en/quantify.js").catch(() => module.exports)
]);
RelativeTimeFormat.addLocale({locale: "en", long, short, narrow, quantify});
return RelativeTimeFormat;
}
https://beta.observablehq.com/@mbostock/relative-time-formatting
The above code is unfortunately specific to the en
locale, as other locales have slightly different sets of files. (And it’d be faster to load one file per locale rather than four, of course.)
If you included AMD/UMD bundles for the locales (and perhaps changed the AMD to export the RelativeTimeFormat function directly rather than wrapping it in an object… and provided an unpkg/jsdelivr entry point in the package.json too), then I could simplify nicely:
RelativeTimeFormat = {
if (Intl.RelativeTimeFormat) return Intl.RelativeTimeFormat;
const [RelativeTimeFormat, locale] = await Promise.all([
require("relative-time-format@0.1"),
require("relative-time-format@0.1/locale/en/index.js")
]);
RelativeTimeFormat.addLocale(locale);
return RelativeTimeFormat;
}
Would you be interested in supporting this usage pattern? I might be able to assist. Alternatively I could distribute bundled locales as a separate library if you prefer.
Hi.
and perhaps changed the AMD to export the RelativeTimeFormat function directly rather than wrapping it in an object…
Oh, that's a good point.
I'll see if Webpack can do that in webpack.config.js
.
Will have to bump major verson for that change.
and provided an unpkg/jsdelivr entry point in the package.json too
What's that?
Is it some kind of an alternative for index/module
fields but one that executes in a browser?
If yes, you could provide me a link to what it is and how to implement such a feature.
Regarding locales though, I guess it wouldn't work because there can only be one entry per package.
Or maybe a multi-package hack would work?
The one when placing package.json
files in sub-directories so that bundlers/browsers could have their specific redirects for those sub-directories.
I guess it could work.
Would you be interested in supporting this usage pattern?
Well, it won't hurt, so why not.
Don't know why don't people just use the freaking bundlers in 2k19 but whatever.
I get such requests from various people for my libraries.
Whatever their reasons are...
So, I guess the build script will have to build all locales too which means running webpack
for each of the locales/${locale}
folder which means it will have to be a javascript file and I guess it will have to use Webpack Node.js API for that (not command line).
And also generate-locale-messages.js
will have to generate package.json
files for each locale subdirectory having entries for main
CommonJS, module
ES6 and whatever
for browsers.
So I guess it can be done.
It will be delayed for a significant time though I guess because currently I have other things going.
You can take this task over if you want.
Otherwise I don't guarantee any estimates whatsoever.
To define it more cleanly:
- Modify
generate-locale-messages.js
so that it createspackage.json
files for each locale subdirectory.module
field can be omitted for now I guess.main
field must point toindex.js
. The other (browser-specific) field must point to somebundle.js
file which will get generated later. - Run
npm run generate-locale-messages
and see if it works. - Create some
generate-locale-bundles.js
file insidebin
directory which will list all locales in thelocale
folder and will generate a "bundle" for each locale. For example, it could use Webpack Node.js API to call webpack compile in afor locale of locales
loop. Create a "script" entry inpackage.json
for the command. - Run
npm run generate-locale-bundles
and see if it works. - Add
generate-locale-bundles
to thebuild
script. - See if the main bundle could export
RelativeTimeFormat
as a "direct" variable instead of it being a "default" (or something else). If it can then implement it bumping major version of the library.
I sketched it out here using Rollup:
https://github.com/mbostock/relative-time-format-locale
The unpkg
and jsdelivr
entry points are, regrettably, the closest thing we have now in package.json for specifying a web-friendly entry point. The main
entry point is typically CommonJS for Node; the module
entry point is non-minified/non-bundled ES modules (and often slightly non-standard, such as missing the .js
file extension on import specifiers, or using bare module specifiers that are not yet supported in browsers); the browser
entry point is used by Browserify, etc. Unpkg has some experimental support for rewriting ES module imports to make them work in browsers mjackson/unpkg#24, but it’s not entirely stable and doesn’t perform well in practice if the code isn’t pre-bundled.
In regards to the unpkg
and jsdelivr
entry points, I was referring to the library itself rather than the locales, so that with AMD you can require by package name rather than needing the path to the exact file. If the require implementation (such as d3-require as used by Observable) can determine where the AMD lives from the package.json, then you can say:
require("relative-time-format")
But if the package.json only specifies CommonJS (main
) and ES Module (module
) entry points, then you need to specify the full path:
require("relative-time-format/bundle/relative-time-format.min.js")
Of course, if you use UMD for the main
entry point, and your code doesn’t require any Node-specific APIs, then you don’t need the unpkg
or jsdelivr
entry points to distribute something that will work in a browser.
For the locales, I was assuming you’d still have to know the full path rather than relying on a shorthand definition in the package.json (though as you say, you could do this by publishing a zillion packages… but my sense is that would be overkill just for the convenience).
Don't know why don't people just use the freaking bundlers in 2k19 but whatever.
Simple: bundlers aren’t web standards; they aren’t built-in to the browser. The web standard answer for this is to ship ES modules and use ES import
instead of require/AMD. The challenge is we don’t yet have a good convention for libraries to distribute ES modules: the module
entry point in the package.json typically refers to non-minified ES module source (which is good if you’re going to run it through a bundler, but bad if you want to consume it directly on the web), and we need browsers to support import maps so that libraries can distribute ES modules with external dependencies expressed as bare specifiers (e.g., import from "foo"
rather than import from "path/to/foo.js"
).
@mbostock Oh shoot, so you're the creator of that very popular "d3" library (which I haven't checked out yet). Oh fck. Ahem.
No wonder your comments are so elaborate.
No shit I'll implement anything you say.
Assume full authority over me.
So, rollup.
Yeah, I heard about it, and that it's more advanced than Webpack.
I guess we better migrate to it then.
I'm just being old-school with Webpack here.
For the locales, I was assuming you’d still have to know the full path rather than relying on a shorthand definition in the package.json (though as you say, you could do this by publishing a zillion packages… but my sense is that would be overkill just for the convenience).
So for locales you'd be fine with something like require('xxx/locales/en/bundle.js')
?
Fine with me.
Simple: bundlers aren’t web standards; they aren’t built-in to the browser.
Oh, so it's the future everyone's talking about.
Makes sense.
I'm not following trends anymore.
Well, a lot of useful info you wrote which I'm not an expert in so whatever, just tell me what to do.
So, how shall we proceed then?
If I get it right, the repo you linked is building an "entry" file for each locale having:
export {default} from "relative-time-format/locale/${locale}/index.js
which can later be consumed by rollup.
These files are put in the build
folder.
You also generate rollup.config.js
.
What's left is calling Rollup for each of these build/locale.js
files which will build them into dist
folder (and also the main library export).
I guess Rollup has programmatic API similar to Webpack's.
So, basically, you already wrote it all (and that was fast).
Do you need me to do anything?
@mbostock So, your comment got me thinking: why do we have a directory with files for each locale?
It looks elegant but isn't much practical.
I toyed with the locale data generation script today and modified it to output a single .js
file for a locale instead of a directory with files.
Example for English:
relative-time-format/locale/en.js
var long = {
"year": {
"previous": "last year",
"current": "this year",
"next": "next year",
"past": {
"one": "{0} year ago",
"other": "{0} years ago"
},
"future": {
"one": "in {0} year",
"other": "in {0} years"
}
},
"quarter": {
"previous": "last quarter",
"current": "this quarter",
"next": "next quarter",
"past": {
"one": "{0} quarter ago",
"other": "{0} quarters ago"
},
"future": {
"one": "in {0} quarter",
"other": "in {0} quarters"
}
},
"month": {
"previous": "last month",
"current": "this month",
"next": "next month",
"past": {
"one": "{0} month ago",
"other": "{0} months ago"
},
"future": {
"one": "in {0} month",
"other": "in {0} months"
}
},
"week": {
"previous": "last week",
"current": "this week",
"next": "next week",
"past": {
"one": "{0} week ago",
"other": "{0} weeks ago"
},
"future": {
"one": "in {0} week",
"other": "in {0} weeks"
}
},
"day": {
"previous": "yesterday",
"current": "today",
"next": "tomorrow",
"past": {
"one": "{0} day ago",
"other": "{0} days ago"
},
"future": {
"one": "in {0} day",
"other": "in {0} days"
}
},
"hour": {
"current": "this hour",
"past": {
"one": "{0} hour ago",
"other": "{0} hours ago"
},
"future": {
"one": "in {0} hour",
"other": "in {0} hours"
}
},
"minute": {
"current": "this minute",
"past": {
"one": "{0} minute ago",
"other": "{0} minutes ago"
},
"future": {
"one": "in {0} minute",
"other": "in {0} minutes"
}
},
"second": {
"current": "now",
"past": {
"one": "{0} second ago",
"other": "{0} seconds ago"
},
"future": {
"one": "in {0} second",
"other": "in {0} seconds"
}
}
}
var short = {
"year": {
"previous": "last yr.",
"current": "this yr.",
"next": "next yr.",
"past": "{0} yr. ago",
"future": "in {0} yr."
},
"quarter": {
"previous": "last qtr.",
"current": "this qtr.",
"next": "next qtr.",
"past": {
"one": "{0} qtr. ago",
"other": "{0} qtrs. ago"
},
"future": {
"one": "in {0} qtr.",
"other": "in {0} qtrs."
}
},
"month": {
"previous": "last mo.",
"current": "this mo.",
"next": "next mo.",
"past": "{0} mo. ago",
"future": "in {0} mo."
},
"week": {
"previous": "last wk.",
"current": "this wk.",
"next": "next wk.",
"past": "{0} wk. ago",
"future": "in {0} wk."
},
"day": {
"previous": "yesterday",
"current": "today",
"next": "tomorrow",
"past": {
"one": "{0} day ago",
"other": "{0} days ago"
},
"future": {
"one": "in {0} day",
"other": "in {0} days"
}
},
"hour": {
"current": "this hour",
"past": "{0} hr. ago",
"future": "in {0} hr."
},
"minute": {
"current": "this minute",
"past": "{0} min. ago",
"future": "in {0} min."
},
"second": {
"current": "now",
"past": "{0} sec. ago",
"future": "in {0} sec."
}
}
var narrow = {
"year": {
"previous": "last yr.",
"current": "this yr.",
"next": "next yr.",
"past": "{0} yr. ago",
"future": "in {0} yr."
},
"quarter": {
"previous": "last qtr.",
"current": "this qtr.",
"next": "next qtr.",
"past": {
"one": "{0} qtr. ago",
"other": "{0} qtrs. ago"
},
"future": {
"one": "in {0} qtr.",
"other": "in {0} qtrs."
}
},
"month": {
"previous": "last mo.",
"current": "this mo.",
"next": "next mo.",
"past": "{0} mo. ago",
"future": "in {0} mo."
},
"week": {
"previous": "last wk.",
"current": "this wk.",
"next": "next wk.",
"past": "{0} wk. ago",
"future": "in {0} wk."
},
"day": {
"previous": "yesterday",
"current": "today",
"next": "tomorrow",
"past": {
"one": "{0} day ago",
"other": "{0} days ago"
},
"future": {
"one": "in {0} day",
"other": "in {0} days"
}
},
"hour": {
"current": "this hour",
"past": "{0} hr. ago",
"future": "in {0} hr."
},
"minute": {
"current": "this minute",
"past": "{0} min. ago",
"future": "in {0} min."
},
"second": {
"current": "now",
"past": "{0} sec. ago",
"future": "in {0} sec."
}
}
module.exports = {
locale: "en",
long: long,
short: short,
narrow: narrow,
quantify: function(n){var r=!String(n).split(".")[1];return 1==n&&r?"one":"other"}
}
What do you think: is it better now?
I guess it is.
And the require statement stays the same: require('relative-time-format/locale/en')
.
I will also replace Webpack bundling script with the Rollup one later.
Is there anything else besides generating locale bundles and using Rollup with "direct export" for the library?
I thought you mentioned something about special package.json
entries (or you didn't).
@mbostock
So I re-thought the locales
folder format again today and re-did locale data as JSON files with quantify
functions being included in the main code (because they're not JSON).
The size of all quantify functions is about 5 kilobytes so it's not a lot.
Also replaced Webpack with Rollup and now it exports properly (added to README).
Released relative-time-format@0.2.0
.
As for the unpkg
entry in package.json
, I saw the thread:
mjackson/unpkg#93
But it's still not clear there whether it's supported or not.
Anyway, I consider this issue done and am closing it.
If there's anything left then you can answer here.