├── .gitignore ├── .travis.yml ├── Brocfile.js ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── app.js ├── broccoli └── postcss.js ├── config ├── index.js ├── lib-sizes.json └── polyfill-service.json ├── i18n ├── cs-CZ.yaml ├── en-US.yaml ├── es-AR.yaml ├── fr-FR.yaml ├── ja-JP.yaml ├── pt-BR.yaml └── sv-SE.yaml ├── lib ├── component.js ├── examples.js ├── hbs.js ├── helpers.js ├── messages.js ├── package-meta.js ├── utils.js └── watcher.js ├── middleware ├── index.js ├── intl.js └── polyfills.js ├── package-lock.json ├── package.json ├── public ├── css │ ├── style.css │ └── syntax.css ├── favicon.ico ├── img │ ├── arrow-down.svg │ ├── arrow-right.svg │ ├── dust.svg │ ├── ember.svg │ ├── handlebars.svg │ ├── js.svg │ ├── logo-element.svg │ ├── logo-splash.svg │ ├── logo.svg │ ├── react.svg │ └── splash-head.jpg ├── js │ ├── components │ │ └── table-of-contents.jsx │ ├── home.jsx │ ├── integration.jsx │ ├── main.js │ └── toc.jsx └── vendor │ └── highlight │ ├── CHANGES.md │ ├── LICENSE │ ├── README.md │ ├── README.ru.md │ └── highlight.pack.js ├── routes ├── about.js ├── dust.js ├── github.js ├── guides.js ├── handlebars.js ├── home.js ├── index.js ├── integrations.js └── react.js ├── server.js ├── shared └── components │ ├── code-block.jsx │ ├── dust-example.jsx │ ├── dust-output.jsx │ ├── example-container.jsx │ ├── example.jsx │ ├── handlebars-example.jsx │ ├── handlebars-output.jsx │ ├── locale-select.jsx │ ├── react-example.jsx │ ├── splash-example.jsx │ └── tabs.jsx ├── tasks └── filesize.js ├── tests ├── fixtures │ └── modules │ │ ├── bar.js │ │ ├── foo.js │ │ └── index.js ├── functional │ ├── includes │ │ ├── date.mock.js │ │ ├── fn.bind.polyfill.js │ │ └── html.event.polyfill.js │ ├── integration-tests.js │ ├── splash-example.js │ └── utils │ │ └── casper-setup.js ├── health-check.js ├── istanbul-hook.js └── unit │ ├── lib.component.js │ ├── lib.examples.js │ ├── lib.helpers.js │ ├── lib.messages.js │ ├── lib.package-meta.js │ ├── lib.utils.js │ └── middleware.intl.js └── views ├── examples ├── dust │ ├── custom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.dust │ ├── date │ │ ├── context.js │ │ └── template.dust │ ├── dateCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.dust │ ├── intl │ │ ├── context.js │ │ └── template.dust │ ├── message │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.dust │ ├── number │ │ ├── context.js │ │ └── template.dust │ ├── numberCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.dust │ ├── relative │ │ ├── context.js │ │ └── template.dust │ ├── relativeCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.dust │ ├── relativeStyle │ │ ├── context.js │ │ └── template.dust │ ├── relativeUnits │ │ ├── context.js │ │ └── template.dust │ └── time │ │ ├── context.js │ │ └── template.dust ├── ember │ ├── date │ │ ├── context.js │ │ └── template.hbs │ ├── message │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── number │ │ ├── context.js │ │ └── template.hbs │ ├── relative │ │ ├── context.js │ │ └── template.hbs │ ├── relativeStyle │ │ ├── context.js │ │ └── template.hbs │ └── relativeUnits │ │ ├── context.js │ │ └── template.hbs ├── handlebars │ ├── custom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── date │ │ ├── context.js │ │ └── template.hbs │ ├── dateCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── intl │ │ ├── context.js │ │ └── template.hbs │ ├── message │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── number │ │ ├── context.js │ │ └── template.hbs │ ├── numberCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── relative │ │ ├── context.js │ │ └── template.hbs │ ├── relativeCustom │ │ ├── context.js │ │ ├── meta.yaml │ │ └── template.hbs │ ├── relativeStyle │ │ ├── context.js │ │ └── template.hbs │ └── relativeUnits │ │ ├── context.js │ │ └── template.hbs └── react │ ├── custom │ ├── component.jsx │ └── meta.yaml │ ├── date │ └── component.jsx │ ├── dateCustom │ ├── component.jsx │ └── meta.yaml │ ├── message │ ├── component.jsx │ └── meta.yaml │ ├── messageHTML │ ├── component.jsx │ └── meta.yaml │ ├── messageNested │ ├── component.jsx │ └── meta.yaml │ ├── number │ └── component.jsx │ ├── numberCustom │ ├── component.jsx │ └── meta.yaml │ ├── relative │ └── component.jsx │ ├── relativeCustom │ ├── component.jsx │ └── meta.yaml │ ├── relativeStyle │ └── component.jsx │ └── relativeUnits │ └── component.jsx ├── layouts └── main.hbs ├── pages ├── about.hbs ├── dust.hbs ├── github.hbs ├── guides │ ├── basic-i18n.hbs │ ├── index.hbs │ ├── message-syntax.hbs │ └── runtime-environments.hbs ├── handlebars.hbs ├── home.hbs ├── integrations.hbs └── react.hbs └── partials ├── analytics.hbs ├── example.hbs ├── foot.hbs ├── formatjs.hbs ├── guides-list.hbs ├── integrations-list.hbs ├── integrations ├── features.hbs ├── load-locale-data-browser.hbs ├── load-locale-data-node.hbs ├── note-intl-browser.hbs ├── note-intl-node.hbs ├── package-install.hbs ├── package-meta.hbs ├── see-custom-formats.hbs └── see-guide.hbs ├── nav.hbs ├── npm-badge.hbs ├── polyfills.hbs ├── purecss.hbs ├── react.hbs └── syntax-highlighting.hbs /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts/ 2 | build 3 | node_modules/ 4 | tmp/ 5 | .DS_Store 6 | npm-debug.log 7 | deploy.json 8 | grasshopper.json 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '8' 5 | - '10' 6 | deploy: 7 | provider: script 8 | script: NOW_ALIAS=formatjs.io ./node_modules/.bin/now-travis 9 | skip_cleanup: true 10 | on: 11 | branch: master 12 | env: 13 | global: 14 | secure: iwLZFsYM6NYa6jD2awa7m2nMC/jqfCyy7dofloV55OGxg18NkzH2WDxift0qYECb+BNuMN7fHbixfxXTxAyC3JOcKXHal4X2KrwmYoVpSHURtbo83/0ul4MRPr31S1q8uwYYIDAtLxPGcQXZnhtaSgG/d3PJQeGwOI9e6fxOJJA= 15 | -------------------------------------------------------------------------------- /Brocfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var autoprefixer = require('autoprefixer-core'); 4 | var compileJSX = require('broccoli-react'); 5 | var compileModules = require('broccoli-es6-module-transpiler'); 6 | var concatTree = require('broccoli-concat'); 7 | var customProperties = require('postcss-custom-properties'); 8 | var mergeTrees = require('broccoli-merge-trees'); 9 | var postcss = require('./broccoli/postcss'); 10 | var unwatchedTree = require('broccoli-unwatched-tree'); 11 | var Funnel = require('broccoli-funnel'); 12 | 13 | var config = require('./config'); 14 | 15 | // ----------------------------------------------------------------------------- 16 | 17 | function copy(tree, mappings) { 18 | var trees = Object.keys(mappings).map(function (srcDir) { 19 | var destDir = mappings[srcDir]; 20 | return new Funnel(tree, {srcDir: srcDir, destDir: destDir}); 21 | }); 22 | 23 | return mergeTrees(trees); 24 | } 25 | 26 | // -- Shared ------------------------------------------------------------------- 27 | 28 | var shared = compileJSX('shared/', { 29 | transform: { 30 | es6module: true, 31 | harmony : true 32 | } 33 | }); 34 | 35 | // -- Server ------------------------------------------------------------------- 36 | 37 | var server = compileModules(shared, { 38 | description: 'ServerModules', 39 | formatter : 'commonjs', 40 | output : 'server/' 41 | }); 42 | 43 | // -- Client ------------------------------------------------------------------- 44 | 45 | var node_modules = unwatchedTree('node_modules/'); 46 | 47 | var vendor = new Funnel('public/vendor/', { 48 | srcDir : '/', 49 | destDir: '/vendor' 50 | }); 51 | 52 | function localeDataTree(srcDir, outputFile) { 53 | // Create list of locale data filenames for all of the locales the app 54 | // supports, which is a small subset of all the locales Format.js libs 55 | // support. 56 | var localeDataFiles = config.availableLocales.map(function (locale) { 57 | return locale.split('-')[0] + '.js'; 58 | }); 59 | 60 | return new Funnel(node_modules, { 61 | srcDir : srcDir, 62 | destDir: '/', 63 | files : localeDataFiles 64 | }); 65 | } 66 | 67 | var formatjsLocaleData = mergeTrees([ 68 | 'dust-intl', 69 | 'handlebars-intl', 70 | 'react-intl' 71 | ].map(function (integration) { 72 | return concatTree(localeDataTree(integration + '/dist/locale-data'), { 73 | inputFiles: ['*.js'], 74 | outputFile: '/vendor/formatjs/' + integration + '-locale-data.js' 75 | }); 76 | })); 77 | 78 | vendor = mergeTrees([ 79 | copy(node_modules, { 80 | 'es6-shim': '/vendor/es6-shim', 81 | 82 | 'dustjs-linkedin/dist': '/vendor/dust', 83 | 'handlebars/dist' : '/vendor/handlebars', 84 | 'jquery/dist' : '/vendor/jquery', 85 | 'react/dist' : '/vendor/react', 86 | 87 | 'dust-intl/dist' : '/vendor/dust-intl', 88 | 'handlebars-intl/dist' : '/vendor/handlebars-intl', 89 | 'react-intl/dist' : '/vendor/react-intl' 90 | }), 91 | 92 | vendor, 93 | formatjsLocaleData 94 | ], {overwrite: true}); 95 | 96 | var pubRoot = new Funnel('public/', { 97 | srcDir : '/', 98 | destDir: '/', 99 | include: [/^[^/.]+\..+$/] 100 | }); 101 | 102 | var css = new Funnel('public/css/', { 103 | srcDir : '/', 104 | destDir: '/css' 105 | }); 106 | 107 | css = postcss(css, { 108 | processors: [ 109 | customProperties(), 110 | autoprefixer() 111 | ] 112 | }); 113 | 114 | var img = new Funnel('public/img/', { 115 | srcDir : '/', 116 | destDir: '/img' 117 | }); 118 | 119 | var js = compileJSX('public/js/', { 120 | transform: { 121 | es6module: true, 122 | harmony : true 123 | } 124 | }); 125 | 126 | js = compileModules(mergeTrees([shared, js]), { 127 | description: 'ClientModules', 128 | formatter : 'bundle', 129 | output : '/js/app.js', 130 | sourceRoot : '/public/' 131 | }); 132 | 133 | var client = new Funnel(mergeTrees([vendor, pubRoot, css, img, js]), { 134 | srcDir : '/', 135 | destDir: 'client' 136 | }); 137 | 138 | // ----------------------------------------------------------------------------- 139 | 140 | module.exports = mergeTrees([server, client]); 141 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | grunt.initConfig({ 5 | filesize: { 6 | intl: { 7 | files: [{ 8 | cwd: 'node_modules/', 9 | src: [ 10 | 'intl/*.min.js', 11 | 'react-intl/dist/*.min.js', 12 | 'dust-intl/dist/*.min.js', 13 | 'handlebars-intl/dist/*.min.js' 14 | ], 15 | dest: 'config/lib-sizes.json' 16 | }] 17 | } 18 | }, 19 | 20 | curl: { 21 | polyfill_service: { 22 | src: 'https://polyfills.yahooapis.com/meta.json', 23 | dest: 'config/polyfill-service.json' 24 | } 25 | }, 26 | 27 | clean: { 28 | build: 'build/', 29 | tmp: 'tmp/' 30 | }, 31 | 32 | broccoli_build: { 33 | assets: { 34 | dest: 'build/' 35 | } 36 | }, 37 | 38 | casper: { 39 | functional: { 40 | files: { 41 | 'artifacts/test/functional/results.xml': ['tests/functional/*.js'] 42 | }, 43 | options: { 44 | test: true, 45 | includes: 'tests/functional/utils/casper-setup.js', 46 | host: grunt.option('host') || 'localhost:5000' 47 | } 48 | } 49 | }, 50 | 51 | shell: { 52 | health_check: { 53 | command: 'mkdir -p artifacts/test/health-check &&' + 54 | ' ./node_modules/.bin/mocha --reporter tap tests/health-check.js --host=' + (grunt.option('host') || 'localhost:5000') + 55 | ' | tee artifacts/test/health-check/results.tap' 56 | } 57 | } 58 | }); 59 | 60 | grunt.loadNpmTasks('grunt-contrib-clean'); 61 | grunt.loadNpmTasks('grunt-broccoli-build'); 62 | grunt.loadNpmTasks('grunt-casper'); 63 | grunt.loadNpmTasks('grunt-curl'); 64 | grunt.loadNpmTasks('grunt-shell-spawn'); 65 | grunt.loadTasks('tasks/'); 66 | 67 | grunt.registerTask('build', ['clean', 'broccoli_build']); 68 | 69 | grunt.registerTask('functional.tests', ['casper:functional']); 70 | grunt.registerTask('health.check', ['shell:health_check']); 71 | 72 | grunt.registerTask('update_polyfill_service', ['curl:polyfill_service']); 73 | 74 | grunt.registerTask('default', ['build']); 75 | }; 76 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014 Yahoo! Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the Yahoo! Inc. nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL YAHOO! INC. BE LIABLE FOR ANY 22 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FormatJS 2 | ======== 3 | 4 | [![Build Status](https://img.shields.io/travis/yahoo/formatjs-site/master.svg?style=flat-square)](https://travis-ci.org/yahoo/formatjs-site) 5 | [![Dependency Status](https://img.shields.io/gemnasium/yahoo/formatjs-site.svg?style=flat-square)](https://gemnasium.com/yahoo/formatjs-site) 6 | 7 | Website for Yahoo's JavaScript internationalization suite. 8 | 9 | Running Locally 10 | --------------- 11 | 12 | This is a Node.js app which uses Express. 13 | 14 | ``` 15 | $ npm install 16 | $ npm start 17 | ``` 18 | 19 | To run the health checks: 20 | 21 | ``` 22 | $ grunt health.check 23 | ``` 24 | 25 | By default, it will run the tests using the local instance (running on port 5000) 26 | But you can also specify a remote host: 27 | 28 | ``` 29 | $ grunt health.check --host=foo 30 | ``` 31 | 32 | To run the functional tests, you must first install `grunt-casper` manually: 33 | 34 | ``` 35 | $ npm install grunt-casper@0.4.2 36 | ``` 37 | 38 | The `grunt-casper` package was removed from the list of dev dependencies because 39 | it adds 150MB to the install, which slows down the pace of development, 40 | especially if you don't need to run the functional tests. Because of this, 41 | grunt will complain every time you invoke it, but you can disregard the warning 42 | message it outputs. 43 | 44 | To run, the functional tests, simply execute the `functional.tests` grunt task: 45 | 46 | ``` 47 | $ grunt functional.tests 48 | ``` 49 | 50 | Likewise, it will run the tests using the local instance (running on port 5000) 51 | Again, you can also specify a remote host: 52 | 53 | ``` 54 | $ grunt functional.tests --host=foo 55 | ``` 56 | 57 | License 58 | ------- 59 | 60 | This software is free to use under the Yahoo Inc. BSD license. See the [LICENSE file](LICENSE.md) for license text and copyright information. 61 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* global Intl, IntlPolyfill */ 2 | 'use strict'; 3 | 4 | var config = require('./config'); 5 | 6 | // -- Configure JavaScript Runtime --------------------------------------------- 7 | 8 | var hasNativePromise = !!global.Promise; 9 | 10 | require('es6-shim'); 11 | 12 | // es6-shim provides a Promise implementation, but it's not very good. 13 | if (!hasNativePromise) { 14 | global.Promise = require('promise') 15 | } 16 | 17 | var areIntlLocalesSupported = require('intl-locales-supported'); 18 | 19 | if (global.Intl) { 20 | // Determine if the built-in `Intl` has the locale data we need. 21 | if (!areIntlLocalesSupported(config.availableLocales)) { 22 | // `Intl` exists, but it doesn't have the data we need, so load the 23 | // polyfill and replace the constructors with need with the polyfill's. 24 | require('intl'); 25 | Intl.NumberFormat = IntlPolyfill.NumberFormat; 26 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; 27 | } 28 | } else { 29 | // No `Intl`, so use and load the polyfill. 30 | global.Intl = require('intl'); 31 | } 32 | 33 | // -- FormatJS Libs ------------------------------------------------------------ 34 | 35 | global.React = require('react/addons'); 36 | global.ReactIntl = require('react-intl'); 37 | global.Handlebars = require('handlebars'); 38 | global.HandlebarsIntl = require('handlebars-intl'); 39 | global.dust = require('dustjs-linkedin'); 40 | global.DustIntl = require('dust-intl'); 41 | 42 | global.DustIntl.registerWith(global.dust); 43 | 44 | // ----------------------------------------------------------------------------- 45 | 46 | var path = require('path'); 47 | var express = require('express'); 48 | var expstate = require('express-state'); 49 | var pathToRegexp = require('path-to-regexp'); 50 | 51 | var hbs = require('./lib/hbs'); 52 | var middleware = require('./middleware'); 53 | var routes = require('./routes'); 54 | 55 | // -- Configure Express App ---------------------------------------------------- 56 | 57 | var app = module.exports = express(); 58 | 59 | expstate.extend(app); 60 | 61 | app.set('name', 'FormatJS'); 62 | app.set('port', config.port); 63 | app.set('polyfill service', config.polyfillService); 64 | app.set('available locales', config.availableLocales); 65 | app.set('default locale', 'en-US'); 66 | app.set('state namespace', 'APP'); 67 | 68 | app.enable('strict routing'); 69 | app.enable('case sensitive routing'); 70 | 71 | app.engine(hbs.extname, hbs.engine); 72 | app.set('view engine', hbs.extname); 73 | app.set('views', config.dirs.views); 74 | 75 | if (app.get('env') === 'development') { 76 | // This will watch files during development and automatically re-build. 77 | app.watcher = require('./lib/watcher'); 78 | } 79 | 80 | // -- Middleware --------------------------------------------------------------- 81 | 82 | var router = express.Router({ 83 | caseSensitive: app.get('case sensitive routing'), 84 | strict : app.get('strict routing') 85 | }); 86 | 87 | if (app.get('env') === 'development') { 88 | app.use(middleware.logger('tiny')); 89 | } 90 | 91 | app.use(middleware.compress()); 92 | app.use(middleware.favicon(path.join('./public/favicon.ico'))); 93 | app.use(middleware.static(path.join(config.dirs.build, 'client'), {maxage: '1m'})); 94 | app.use(router); 95 | app.use(middleware.slash()); 96 | 97 | // -- Routes ------------------------------------------------------------------- 98 | 99 | var route = router.route.bind(router); 100 | 101 | router.use(middleware.fixBadSafari); 102 | router.use(middleware.intl); 103 | router.use(middleware.polyfills); 104 | 105 | route('/guide/').get(routes.redirect('/guides/')); 106 | route('/react/').get(routes.redirect('https://github.com/yahoo/react-intl')); 107 | route('/ember/').get(routes.redirect('https://github.com/yahoo/ember-intl')); 108 | 109 | routes.home(route('/')); 110 | routes.about(route('/about/')); 111 | routes.guides(route('/guides/:guide?/')); 112 | routes.integrations(route('/integrations/')); 113 | routes.github(route('/github/')); 114 | 115 | routes.react(route('/react/v1/')); 116 | routes.handlebars(route('/handlebars/')); 117 | routes.dust(route('/dust/')); 118 | 119 | app.getRoute = function (routeName) { 120 | var layer = router.stack.find(function (layer) { 121 | return layer.route && layer.route.name === routeName; 122 | }); 123 | 124 | if (!layer) { 125 | throw new Error('No route name: ' + routeName); 126 | } 127 | 128 | return layer.route; 129 | }; 130 | 131 | app.getPathTo = function (routeName, context) { 132 | var toPath = pathToRegexp.compile(app.getRoute(routeName).path); 133 | return toPath(context); 134 | }; 135 | 136 | // -- Locals ------------------------------------------------------------------- 137 | 138 | Object.assign(app.locals, { 139 | brand : app.get('name'), 140 | tagline: 'Internationalize your web apps on the client & server.', 141 | 142 | min: app.get('env') === 'production' ? '.min' : '', 143 | 144 | menuItems : ['guides', 'integrations', 'github'].map(app.getRoute), 145 | footMenuItems: ['home', 'about', 'guides', 'integrations', 'github'].map(app.getRoute), 146 | 147 | helpers: { 148 | pathTo: function (name, options) { 149 | return app.getPathTo(name, options.hash); 150 | } 151 | } 152 | }); 153 | 154 | if (app.get('env') === 'production') { 155 | app.locals.analytics = config.ga; 156 | } 157 | -------------------------------------------------------------------------------- /broccoli/postcss.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = Object.assign || require('object.assign'); 4 | 5 | var Filter = require('broccoli-filter'), 6 | postcss = require('postcss'), 7 | util = require('util'); 8 | 9 | module.exports = PostCSSFilter; 10 | 11 | // ----------------------------------------------------------------------------- 12 | 13 | function PostCSSFilter(inputTree, options) { 14 | if (!(this instanceof PostCSSFilter)) { 15 | return new PostCSSFilter(inputTree, options); 16 | } 17 | 18 | this.inputTree = inputTree; 19 | this.options = assign({processors: []}, options); 20 | } 21 | 22 | util.inherits(PostCSSFilter, Filter); 23 | 24 | PostCSSFilter.prototype.extensions = ['css']; 25 | PostCSSFilter.prototype.targetExtension = 'css'; 26 | 27 | PostCSSFilter.prototype.processString = function (css, relPath) { 28 | var processor = postcss(); 29 | 30 | var options = assign({ 31 | from: relPath, 32 | to : relPath 33 | }, this.options); 34 | 35 | options.processors.forEach(function (p) { 36 | processor.use(p); 37 | }); 38 | 39 | return processor.process(css, options).css; 40 | }; 41 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var glob = require('glob'); 4 | var path = require('path'); 5 | 6 | exports.port = process.env.PORT || 5000; 7 | 8 | exports.dirs = { 9 | build : path.resolve('build/'), 10 | i18n : path.resolve('i18n/'), 11 | views : path.resolve('views/pages/'), 12 | layouts : path.resolve('views/layouts/'), 13 | partials: path.resolve('views/partials/'), 14 | examples: path.resolve('views/examples/') 15 | }; 16 | 17 | exports.availableLocales = glob.sync('*.yaml', { 18 | cwd: exports.dirs.i18n 19 | }).map(function (file) { 20 | return path.basename(file, '.yaml'); 21 | }); 22 | 23 | exports.ga = 'UA-55722103-1'; 24 | 25 | exports.libSizes = require('./lib-sizes.json'); 26 | 27 | exports.polyfillService = { 28 | hostname: 'polyfills.yahooapis.com', 29 | version : require('./polyfill-service.json').version 30 | }; 31 | -------------------------------------------------------------------------------- /config/lib-sizes.json: -------------------------------------------------------------------------------- 1 | { 2 | "intl/Intl.min.js": { 3 | "bytes": 10005, 4 | "kbs": 9.8 5 | }, 6 | "react-intl/dist/react-intl-with-locales.min.js": { 7 | "bytes": 48859, 8 | "kbs": 47.7 9 | }, 10 | "react-intl/dist/react-intl.min.js": { 11 | "bytes": 8181, 12 | "kbs": 8 13 | }, 14 | "dust-intl/dist/dust-intl-with-locales.min.js": { 15 | "bytes": 48305, 16 | "kbs": 47.2 17 | }, 18 | "dust-intl/dist/dust-intl.min.js": { 19 | "bytes": 7638, 20 | "kbs": 7.5 21 | }, 22 | "handlebars-intl/dist/handlebars-intl-with-locales.min.js": { 23 | "bytes": 48287, 24 | "kbs": 47.2 25 | }, 26 | "handlebars-intl/dist/handlebars-intl.min.js": { 27 | "bytes": 7523, 28 | "kbs": 7.3 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/polyfill-service.json: -------------------------------------------------------------------------------- 1 | {"version":"2.1.9","features":["Array.isArray","Array.prototype.every","Array.prototype.filter","Array.prototype.forEach","Array.prototype.indexOf","Array.prototype.lastIndexOf","Array.prototype.map","Array.prototype.reduce","Array.prototype.reduceRight","Array.prototype.some","Date.now","Date.prototype.toISOString","Function.prototype.bind","Intl","Object.create","Object.defineProperties","Object.defineProperty","Object.freeze","Object.getOwnPropertyNames","Object.keys","Promise","String.prototype.trim"]} -------------------------------------------------------------------------------- /i18n/cs-CZ.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | {takenDate, date, long} {name} {numPhotos, plural, 3 | =0 {nevyfotila} 4 | other {vyfotila} 5 | } {numPhotos, plural, 6 | =0 {žádnou fotku} 7 | one {jednu fotku} 8 | few {# fotky} 9 | other {# fotek} 10 | }. 11 | -------------------------------------------------------------------------------- /i18n/en-US.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | {name} took {numPhotos, plural, 3 | =0 {no photos} 4 | =1 {one photo} 5 | other {# photos} 6 | } on {takenDate, date, long}. 7 | 8 | photosNested: | 9 | {name} took {numPhotos, plural, 10 | =0 {no photos} 11 | =1 {one photo} 12 | other {# photos} 13 | } {takenAgo}. 14 | 15 | comments: | 16 | {numComments, plural, 17 | =0 {no comments} 18 | =1 {1 comment} 19 | other {# comments} 20 | } 21 | 22 | commentsHTML: | 23 | {numComments, plural, 24 | =0 {no comments} 25 | =1 {1 comment} 26 | other {# comments} 27 | } 28 | -------------------------------------------------------------------------------- /i18n/es-AR.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | El {takenDate, date, long}, {name} {numPhotos, plural, 3 | =0 {no} 4 | other {} 5 | } sacó {numPhotos, plural, 6 | =0 {ninguna foto.} 7 | =1 {una foto.} 8 | other {# fotos.} 9 | } 10 | -------------------------------------------------------------------------------- /i18n/fr-FR.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | Le {takenDate, date, long}, {name} {numPhotos, plural, 3 | =0 {n'a pas pris de photographie.} 4 | =1 {a pris une photographie.} 5 | other {a pris # photographies.} 6 | } 7 | -------------------------------------------------------------------------------- /i18n/ja-JP.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | {name}は{takenDate, date, long}に{numPhotos, plural, 3 | =0 {1枚も写真を撮りませんでした。} 4 | =1 {1枚写真を撮りました。} 5 | other {#枚写真を撮りました。} 6 | } 7 | -------------------------------------------------------------------------------- /i18n/pt-BR.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | Em {takenDate, date, long}, {name} {numPhotos, plural, 3 | =0 {não} 4 | other {} 5 | } tirou {numPhotos, plural, 6 | =0 {nenhuma foto.} 7 | =1 {uma foto.} 8 | other {# fotos.} 9 | } 10 | -------------------------------------------------------------------------------- /i18n/sv-SE.yaml: -------------------------------------------------------------------------------- 1 | photos: | 2 | {name} tog {numPhotos, plural, 3 | =0 {inte några foton} 4 | =1 {ett foto} 5 | other {# foton} 6 | } den {takenDate, date, long}. 7 | -------------------------------------------------------------------------------- /lib/component.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | React = require('react'); 6 | 7 | var config = require('../config'); 8 | 9 | exports.render = renderComponent; 10 | exports.require = requireComponent; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | function requireComponent(componentPath) { 15 | // Make sure the symlink is followed for the build/ dir during dev. 16 | var buildDir = fs.realpathSync(config.dirs.build); 17 | 18 | // Assume server-side comonent. 19 | componentPath = path.join(buildDir, 'server/components', componentPath); 20 | 21 | // Retun the default export. 22 | return require(componentPath).default; 23 | } 24 | 25 | function renderComponent(componentPath, props) { 26 | var Component = requireComponent(componentPath); 27 | return React.renderToString(React.createElement(Component, props)); 28 | } 29 | -------------------------------------------------------------------------------- /lib/examples.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var transformJSX = require('react-tools').transform; 6 | var yaml = require('js-yaml'); 7 | 8 | var config = require('../config'); 9 | var utils = require('./utils'); 10 | var component = require('../lib/component'); 11 | 12 | exports.get = getExamples; 13 | exports.render = renderExamples; 14 | 15 | // ----------------------------------------------------------------------------- 16 | 17 | var readDir = utils.promisify(fs.readdir); 18 | var readFile = utils.promisify(fs.readFile); 19 | var stat = utils.promisify(fs.stat); 20 | 21 | var cache = {}; 22 | 23 | function getExamples(type, options) { 24 | if (!type) { 25 | throw new Error('An example type must be provided'); 26 | } 27 | 28 | options || (options = {}); 29 | 30 | var examples = options.cache && cache[type]; 31 | if (examples) { return examples; } 32 | 33 | var examplesDir = path.join(config.dirs.examples, type); 34 | 35 | examples = cache[type] = readDir(examplesDir) 36 | .then(getSubdirs.bind(null, examplesDir)) 37 | .then(processExamples.bind(null, type)); 38 | 39 | return examples.catch(function (e) { 40 | // Remove from cache if there's an error. 41 | delete cache[type]; 42 | throw e; 43 | }); 44 | } 45 | 46 | function renderExamples(examples, intl) { 47 | if (!Array.isArray(examples)) { 48 | throw new Error('An array of examples must be provided to render'); 49 | } 50 | 51 | if (!intl || typeof intl !== 'object') { 52 | throw new Error('An intl object must be provided to render examples'); 53 | } 54 | 55 | return examples.reduce(function (hash, example) { 56 | hash[example.name] = component.render('example-container', { 57 | example: example, 58 | intl : intl 59 | }); 60 | 61 | return hash; 62 | }, {}); 63 | } 64 | 65 | function getSubdirs(dir, entries) { 66 | return Promise.all(entries.map(function (entry) { 67 | return stat(path.join(dir, entry)); 68 | })).then(function (stats) { 69 | return entries.filter(function (entry, i) { 70 | return stats[i].isDirectory(); 71 | }); 72 | }); 73 | } 74 | 75 | function processExamples(type, names) { 76 | return Promise.all(names.map(function (name) { 77 | return Promise.all([ 78 | getMetadata(type, name), 79 | getSourceFiles(type, name) 80 | ]).then(function (files) { 81 | var metadata = files[0]; 82 | var sourceFiles = files[1]; 83 | 84 | var example = { 85 | id : 'ex-' + type + '-' + name, 86 | name : name, 87 | type : type, 88 | meta : metadata, 89 | source: sourceFiles 90 | }; 91 | 92 | // Transform a React example's JSX. 93 | if (sourceFiles.component) { 94 | example.getComponent = wrapComponent(sourceFiles.component); 95 | } 96 | 97 | return example; 98 | }); 99 | })); 100 | } 101 | 102 | function getMetadata(type, name) { 103 | var exampleDir = path.join(config.dirs.examples, type, name); 104 | 105 | return readFile(path.join(exampleDir, 'meta.yaml'), 'utf8') 106 | .then(function (contents) { 107 | return yaml.safeLoad(contents); 108 | }, function (err) { 109 | // Surpress error, okay if an example doesn't have a meta.yaml file. 110 | // Simply return an empty object. 111 | return {}; 112 | }); 113 | } 114 | 115 | function getSourceFiles(type, name) { 116 | var exampleDir = path.join(config.dirs.examples, type, name); 117 | 118 | switch (type) { 119 | case 'dust': 120 | return Promise.all([ 121 | readFile(path.join(exampleDir, 'template.dust'), 'utf8'), 122 | readFile(path.join(exampleDir, 'context.js'), 'utf8') 123 | ]).then(function (sourceFiles) { 124 | return { 125 | template: sourceFiles[0], 126 | context : sourceFiles[1] 127 | }; 128 | }); 129 | 130 | case 'handlebars': 131 | return Promise.all([ 132 | readFile(path.join(exampleDir, 'template.hbs'), 'utf8'), 133 | readFile(path.join(exampleDir, 'context.js'), 'utf8') 134 | ]).then(function (sourceFiles) { 135 | return { 136 | template: sourceFiles[0], 137 | context : sourceFiles[1] 138 | }; 139 | }); 140 | 141 | case 'react': 142 | return Promise.all([ 143 | readFile(path.join(exampleDir, 'component.jsx'), 'utf8') 144 | ]).then(function (sourceFiles) { 145 | return {component: sourceFiles[0]}; 146 | }); 147 | 148 | default: 149 | throw new Error('Unsupported example type: ' + type); 150 | } 151 | } 152 | 153 | function wrapComponent(jsxSource) { 154 | var source = transformJSX(jsxSource, { 155 | harmony: true 156 | }); 157 | 158 | // And in this case we're reusing the compiled source in the server 159 | /*jslint evil: true*/ 160 | return new Function(source + '\nreturn Component;'); 161 | /*jslint evil: false*/ 162 | } 163 | -------------------------------------------------------------------------------- /lib/hbs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var exphbs = require('express-handlebars'), 4 | Handlebars = require('handlebars'), 5 | HandlebarsIntl = require('handlebars-intl'); 6 | 7 | var config = require('../config'), 8 | helpers = require('./helpers'); 9 | 10 | HandlebarsIntl.registerWith(Handlebars); 11 | 12 | module.exports = exphbs.create({ 13 | defaultLayout: 'main', 14 | handlebars : Handlebars, 15 | helpers : helpers, 16 | layoutsDir : config.dirs.layouts, 17 | partialsDir : config.dirs.partials, 18 | extname : '.hbs' 19 | }); 20 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | var Handlebars = require('handlebars'); 5 | var libSizes = require('../config').libSizes; 6 | 7 | var escape = Handlebars.Utils.escapeExpression; 8 | 9 | exports.isEqual = function (lhs, rhs) { 10 | return lhs === rhs; 11 | }; 12 | 13 | exports.setTitle = function (title) { 14 | this.title = title; 15 | }; 16 | 17 | exports.title = function () { 18 | var brand = this.brand; 19 | var title = this.title; 20 | 21 | if (!title) { 22 | return brand; 23 | } 24 | 25 | title = escape(title) + ' — ' + escape(brand); 26 | return new Handlebars.SafeString(title); 27 | }; 28 | 29 | exports.setDescription = function () { 30 | var args = [].slice.call(arguments); 31 | args.pop(); 32 | 33 | this.description = args.join(''); 34 | }; 35 | 36 | exports.description = function () { 37 | var tagline = this.tagline; 38 | var description = this.description; 39 | 40 | if (!description) { 41 | return tagline; 42 | } 43 | 44 | return description; 45 | }; 46 | 47 | exports.code = function (content, options) { 48 | if (typeof content !== 'string') { 49 | options = content; 50 | content = null; 51 | } 52 | 53 | var highlight, lang, classes, open, close; 54 | 55 | // Determine if this helper is being used as an inline or block helper. When 56 | // `options.fn` is defined this helper was invoked as a block helper. 57 | if (options.fn) { 58 | highlight = options.hash.hasOwnProperty('highlight') ? 59 | options.hash.highlight : true; 60 | 61 | lang = highlight ? (content || '') : 'nohighlight'; 62 | classes = options.hash.wrap ? 'code code-wrap' : 'code'; 63 | content = options.fn(this); 64 | 65 | open = '
';
 66 |         close = '
'; 67 | } else { 68 | open = ''; 69 | close = ''; 70 | } 71 | 72 | // Escape code contents. 73 | content = escape(content.trim()); 74 | 75 | return new Handlebars.SafeString(open + content + close); 76 | }; 77 | 78 | exports.npmLink = function (name, options) { 79 | return new Handlebars.SafeString( 80 | '' + 81 | '' + name + '' + 82 | '' 83 | ); 84 | }; 85 | 86 | exports.releaseDownloadUrl = function (name, version) { 87 | return url.format({ 88 | protocol: 'https:', 89 | hostname: 'github.com', 90 | pathname: [ 91 | 'yahoo', name, 'archive', 'v' + version + '.tar.gz' 92 | ].join('/') 93 | }); 94 | }; 95 | 96 | exports.size = function (filename) { 97 | var entry = libSizes[filename]; 98 | 99 | if (!entry) { 100 | throw new Error('No size info for: ' + filename); 101 | } 102 | 103 | if (entry.bytes < 1024) { 104 | return entry.bytes + ' bytes'; 105 | } 106 | 107 | return entry.kbs + ' KB'; 108 | }; 109 | 110 | exports.cdnUrl = function (filename) { 111 | var pkg = filename.slice(0, filename.indexOf('/')); 112 | var version = require(pkg + '/package.json').version; 113 | filename = filename.slice(pkg.length); 114 | 115 | // TODO: Need a way to validate that these are good enough (maybe smoke test?) 116 | return url.format({ 117 | protocol: 'https:', 118 | hostname: 'cdn.rawgit.com', 119 | pathname: ['yahoo', pkg, 'v' + version, filename].join('/') 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /lib/messages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'), 4 | path = require('path'), 5 | yaml = require('js-yaml'); 6 | 7 | var config = require('../config'), 8 | utils = require('./utils'); 9 | 10 | module.exports = getMessages; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | var readFile = utils.promisify(fs.readFile); 15 | 16 | var cache = null; 17 | 18 | function getMessages(options) { 19 | options || (options = {}); 20 | 21 | var messages = options.cache && cache; 22 | if (messages) { return messages; } 23 | 24 | messages = cache = processMessageFiles(config.availableLocales) 25 | .then(hashMessagesByLocale); 26 | 27 | return messages.catch(function (err) { 28 | cache = null; 29 | throw err; 30 | }); 31 | } 32 | 33 | function processMessageFiles(locales) { 34 | return Promise.all(locales.map(function (locale) { 35 | var messagesFile = path.join(config.dirs.i18n, locale) + '.yaml'; 36 | return readFile(messagesFile, 'utf8').then(function (contents) { 37 | return { 38 | locale : locale, 39 | messages: yaml.safeLoad(contents) 40 | }; 41 | }); 42 | })); 43 | } 44 | 45 | function hashMessagesByLocale(entries) { 46 | return entries.reduce(function (hash, entry) { 47 | hash[entry.locale] = entry.messages; 48 | return hash; 49 | }, {}); 50 | } 51 | -------------------------------------------------------------------------------- /lib/package-meta.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = function (pkgName, options) { 6 | var pkg = require(path.join(pkgName, 'package.json')); 7 | 8 | options || (options = {}); 9 | var distPath = options.distPath || path.join(pkg.name, 'dist'); 10 | 11 | return Object.assign({ 12 | name : pkg.name, 13 | version : pkg.version, 14 | description: pkg.description, 15 | hasDownload: true, 16 | 17 | dist: { 18 | main : path.join(distPath, pkg.name + '.min.js'), 19 | withLocales: path.join(distPath, pkg.name + '-with-locales.min.js'), 20 | localeData : path.join(distPath, 'locale-data') 21 | } 22 | }, options); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var STATUS_CODES = require('http'); 4 | 5 | var fs = require('fs'), 6 | path = require('path'); 7 | 8 | exports.error = error; 9 | exports.requireDir = requireDir; 10 | exports.promisify = promisify; 11 | 12 | // ----------------------------------------------------------------------------- 13 | 14 | function error(statusCode, message) { 15 | var err; 16 | 17 | if (message instanceof Error) { 18 | err = message; 19 | } else { 20 | err = new Error(message || STATUS_CODES[statusCode]); 21 | } 22 | 23 | err.status = statusCode; 24 | return err; 25 | } 26 | 27 | function requireDir(dir) { 28 | return fs.readdirSync(dir).reduce(function (modules, filename) { 29 | if (filename === 'index.js' || path.extname(filename) !== '.js') { 30 | return modules; 31 | } 32 | 33 | var moduleName = path.basename(filename, '.js'), 34 | module = require(path.join(dir, moduleName)); 35 | 36 | if (typeof module === 'function' && module.name) { 37 | moduleName = module.name; 38 | } 39 | 40 | modules[moduleName] = module; 41 | return modules; 42 | }, {}); 43 | } 44 | 45 | function promisify(fn) { 46 | return function () { 47 | var args = Array.prototype.slice.call(arguments); 48 | return new Promise(function (resolve, reject) { 49 | args.push(function (err, result) { 50 | if (err) { 51 | reject(err); 52 | } else { 53 | resolve(result); 54 | } 55 | }); 56 | fn.apply(null, args); 57 | }); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /lib/watcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var broccoli = require('broccoli'); 4 | var Watcher = require('broccoli-sane-watcher'); 5 | var fs = require('fs'); 6 | var rimraf = require('rimraf'); 7 | 8 | var config = require('../config'); 9 | 10 | // ----------------------------------------------------------------------------- 11 | 12 | var tree = broccoli.loadBrocfile(); 13 | var builder = new broccoli.Builder(tree); 14 | var watcher = new Watcher(builder); 15 | 16 | function cleanup() { 17 | rmBuildDir(); 18 | builder.cleanup().then(function () { 19 | process.exit(1); 20 | }); 21 | } 22 | 23 | function rmBuildDir() { 24 | rimraf.sync(config.dirs.build); 25 | } 26 | 27 | function mkBuildDir(dir) { 28 | fs.symlinkSync(dir, config.dirs.build, 'dir'); 29 | } 30 | 31 | function onBuildSuccess(results) { 32 | rmBuildDir(); 33 | mkBuildDir(results.directory); 34 | console.log('BUILT - %d ms', Math.floor(results.totalTime / 1e6)); 35 | } 36 | 37 | function onbuildError(err) { 38 | console.log('\x07BUILD ERROR:'); 39 | console.log(err.stack); 40 | console.log(''); 41 | } 42 | 43 | watcher.on('change', onBuildSuccess); 44 | watcher.on('error', onbuildError); 45 | 46 | process.on('SIGINT', cleanup); 47 | process.on('SIGTERM', cleanup); 48 | 49 | module.exports = watcher; 50 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../lib/utils'); 4 | 5 | exports = module.exports = utils.requireDir(__dirname); 6 | 7 | exports.compress = require('compression'); 8 | exports.errorHandler = require('errorhandler'); 9 | exports.favicon = require('serve-favicon'); 10 | exports.fixBadSafari = require('jumanji'); 11 | exports.logger = require('morgan'); 12 | exports.slash = require('express-slash'); 13 | exports.static = require('serve-static'); 14 | -------------------------------------------------------------------------------- /middleware/intl.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getMessages = require('../lib/messages'); 4 | 5 | module.exports = function (req, res, next) { 6 | var app = req.app; 7 | var availableLocales = app.get('available locales'); 8 | var defaultLocale = app.get('default locale'); 9 | var isProduction = app.get('env') === 'production'; 10 | 11 | getMessages({cache: isProduction}).then(function (messages) { 12 | // Use content negotiation to find the best locale. 13 | var locale = req.acceptsLanguages(availableLocales) || defaultLocale; 14 | 15 | // Make the negotiated locale available on the request object. 16 | req.locale = locale; 17 | 18 | // Provide the intl data on the response object. 19 | res.intl = { 20 | availableLocales: availableLocales, 21 | 22 | locales : [locale], 23 | messages: messages 24 | }; 25 | 26 | // Populate the special `data` local for handlebars-intl to use when 27 | // rendering the Handlebars templates. 28 | (res.locals.data || (res.locals.data = {})).intl = res.intl; 29 | 30 | res.expose(res.intl, 'intl'); 31 | setImmediate(next); 32 | }, next); 33 | }; 34 | -------------------------------------------------------------------------------- /middleware/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var url = require('url'); 4 | 5 | module.exports = function (req, res, next) { 6 | var app = req.app; 7 | var polyfillService = app.get('polyfill service'); 8 | var availableLocales = app.get('available locales'); 9 | var isProduction = app.get('env') === 'production'; 10 | 11 | var features = [ 12 | 'Intl' 13 | ].concat(availableLocales.map(function (locale) { 14 | return 'locale-data-' + locale; 15 | })); 16 | 17 | var polyfillUrl = url.format({ 18 | protocol: 'https:', 19 | host : polyfillService.hostname, 20 | pathname: 'polyfill' + (isProduction ? '.min.js' : '.js'), 21 | 22 | query: { 23 | version : polyfillService.version, 24 | features: features.join(','), 25 | } 26 | }); 27 | 28 | res.locals.polyfillUrl = polyfillUrl; 29 | next(); 30 | }; 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formatjs-site", 3 | "version": "1.2.0", 4 | "description": "Documentation for client/server internationalization in JavaScript.", 5 | "private": true, 6 | "main": "server.js", 7 | "engines": { 8 | "node": "6.x.x" 9 | }, 10 | "scripts": { 11 | "postinstall": "grunt", 12 | "test": "istanbul cover --dir ./artifacts/test/coverage --post-require-hook ./tests/istanbul-hook -- _mocha tests/unit/ --require es6-shim --reporter spec", 13 | "start": "node server.js", 14 | "now-build": "grunt", 15 | "now-start": "NODE_ENV=production node server.js" 16 | }, 17 | "dependencies": { 18 | "compression": "^1.0.11", 19 | "dust-intl": "^1.1.0", 20 | "dustjs-linkedin": "^2.4.0", 21 | "errorhandler": "^1.1.1", 22 | "es6-shim": "^0.27.1", 23 | "express": "^4.9.5", 24 | "express-handlebars": "^3.0.0", 25 | "express-slash": "^2.0.1", 26 | "express-state": "^1.2.0", 27 | "glob": "^5.0.3", 28 | "handlebars": "^4.0.11", 29 | "handlebars-intl": "^1.1.1", 30 | "intl": "^0.1.4", 31 | "intl-locales-supported": "^1.0.0", 32 | "jquery": "^3.3.1", 33 | "js-yaml": "^3.2.1", 34 | "jumanji": "^0.1.1", 35 | "morgan": "^1.2.3", 36 | "nodeify": "^1.0.0", 37 | "path-to-regexp": "^1.2.0", 38 | "promise": "^6.1.0", 39 | "react": "^0.13.1", 40 | "react-intl": "^1.2.0", 41 | "react-tools": "^0.13.1", 42 | "serve-favicon": "^2.1.1", 43 | "serve-static": "^1.8.0" 44 | }, 45 | "devDependencies": { 46 | "autoprefixer-core": "^5.1.7", 47 | "broccoli": "^1.1.4", 48 | "broccoli-concat": "^3.7.1", 49 | "broccoli-es6-module-transpiler": "^0.5.0", 50 | "broccoli-filter": "^0.1.7", 51 | "broccoli-funnel": "^2.0.1", 52 | "broccoli-merge-trees": "^0.2.0", 53 | "broccoli-react": "^0.7.1", 54 | "broccoli-sane-watcher": "^1.1.5", 55 | "broccoli-unwatched-tree": "^0.1.1", 56 | "chai": "^4.1.2", 57 | "chai-as-promised": "^4.3.0", 58 | "grunt": "^1.0.3", 59 | "grunt-broccoli-build": "^0.6.0", 60 | "grunt-cli": "^1.3.0", 61 | "grunt-contrib-clean": "^1.1.0", 62 | "grunt-curl": "^2.1.0", 63 | "grunt-shell-spawn": "^0.3.1", 64 | "istanbul": "^0.4.5", 65 | "minimatch": "^3.0.4", 66 | "mocha": "^5.2.0", 67 | "now-travis": "^1.0.0", 68 | "object.assign": "^1.0.1", 69 | "postcss": "^4.0.6", 70 | "postcss-custom-properties": "^3.0.1", 71 | "rimraf": "^2.2.8", 72 | "supertest": "^3.1.0", 73 | "xunit-file": "0.0.6" 74 | }, 75 | "repository": { 76 | "type": "git", 77 | "url": "https://github.com/yahoo/formatjs-site.git" 78 | }, 79 | "author": "Eric Ferraiuolo ", 80 | "contributors": [ 81 | "Tilo Mitra ", 82 | "Juan Dopazo ", 83 | "Clarence Leung ", 84 | "Jason Mitchell ", 85 | "Julien Lecomte " 86 | ], 87 | "license": "BSD", 88 | "bugs": { 89 | "url": "https://github.com/yahoo/formatjs-site/issues" 90 | }, 91 | "homepage": "https://formatjs.io/", 92 | "jshintConfig": { 93 | "node": true, 94 | "browser": true, 95 | "esnext": true, 96 | "predef": [ 97 | "DustIntl", 98 | "dust" 99 | ], 100 | "excludes": [ 101 | "examples/", 102 | "build/", 103 | "tmp/", 104 | "tests/functional/includes/" 105 | ] 106 | }, 107 | "yahoo": { 108 | "bugzilla": { 109 | "product": "yui utilities", 110 | "component": "Intl" 111 | }, 112 | "custodian": { 113 | "email": "jlecomte@yahoo-inc.com", 114 | "url": "https://formatjs.io" 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /public/css/syntax.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --black : rgb(65, 57, 70); 3 | --gray : rgb(95, 90, 99); 4 | --gray-medium : rgb(150, 150, 150); 5 | --gainsboro : rgb(220, 220, 220); 6 | --gray-light : rgb(247, 247, 247); 7 | --white : rgb(255, 255, 255); 8 | --white-alpha : rgba(255, 255, 255, 0.44); 9 | --pink : rgb(173, 104, 216); 10 | --pink-light : rgb(178, 155, 194); 11 | --pink-alpha3 : rgba(208, 146, 247, 0.20); 12 | --pink-alpha2 : rgba(208, 146, 247, 0.15); 13 | --pink-alpha1 : rgba(208, 146, 247, 0.10); 14 | --purple : rgb(131, 57, 194); 15 | --purple-medium: rgb(94, 67, 112); 16 | --purple-dark : rgb(103, 58, 131); 17 | --yellow-js : rgb(247, 223, 30); 18 | --yellow-dark : rgb(197, 166, 0); 19 | } 20 | 21 | .hljs { 22 | color: var(--black); 23 | } 24 | 25 | .hljs-comment, 26 | .hljs-template_comment { 27 | color: var(--gray-medium); 28 | font-style: italic; 29 | } 30 | 31 | .hljs-keyword, 32 | .css .rule .hljs-keyword, 33 | .javascript .hljs-title, 34 | .hljs-subst, 35 | .hljs-request, 36 | .hljs-status { 37 | font-weight: bold; 38 | } 39 | 40 | .hljs-number, 41 | .hljs-hexcolor { 42 | color: var(--yellow-dark) 43 | } 44 | 45 | .hljs-string, 46 | .hljs-tag .hljs-value { 47 | color: var(--purple-dark); 48 | } 49 | 50 | .hljs-title, 51 | .hljs-id { 52 | color: var(--purple); 53 | font-weight: bold; 54 | } 55 | 56 | .javascript .hljs-title, 57 | .hljs-list .hljs-keyword, 58 | .hljs-subst { 59 | font-weight: bold; 60 | } 61 | 62 | .hljs-class .hljs-title, 63 | .hljs-type { 64 | font-weight: bold; 65 | } 66 | 67 | .hljs-attribute, 68 | .hljs-variable { 69 | color: var(--pink); 70 | } 71 | 72 | .hljs-regexp { 73 | color: var(--yellow-dark); 74 | } 75 | 76 | .hljs-built_in { 77 | color: var(--purple); 78 | } 79 | 80 | .hljs-preprocessor, 81 | .hljs-pragma, 82 | .hljs-pi, 83 | .hljs-doctype, 84 | .hljs-shebang, 85 | .hljs-cdata { 86 | font-weight: bold; 87 | } 88 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formatjs/formatjs-site/666a3a0e40b6006e1e20a423cdba087356d4e1f8/public/favicon.ico -------------------------------------------------------------------------------- /public/img/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/img/arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/img/dust.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/img/ember.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 31 | 32 | -------------------------------------------------------------------------------- /public/img/handlebars.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/js.svg: -------------------------------------------------------------------------------- 1 | 2 | 26 | 31 | 32 | -------------------------------------------------------------------------------- /public/img/logo-element.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/react.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /public/img/splash-head.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/formatjs/formatjs-site/666a3a0e40b6006e1e20a423cdba087356d4e1f8/public/img/splash-head.jpg -------------------------------------------------------------------------------- /public/js/components/table-of-contents.jsx: -------------------------------------------------------------------------------- 1 | export default React.createClass({ 2 | displayName: 'TableOfContents', 3 | 4 | selectors: [ 5 | '.secs > h2', 6 | '.secs > h3', 7 | '.secs > h4' 8 | ], 9 | 10 | getLists: function (target, selectorIndex, maxDepth) { 11 | var data = []; 12 | var headers = target.querySelectorAll(this.selectors[selectorIndex]); 13 | var header; 14 | var anchor; 15 | var section; 16 | var childHeaders; 17 | var nextSelector = selectorIndex + 1; 18 | 19 | for (var i = 0, l = headers.length; i < l; i++) { 20 | header = headers[i]; 21 | anchor = {header.textContent}; 22 | section = header.parentNode; 23 | childHeaders = section.querySelectorAll(this.selectors[nextSelector]); 24 | if (childHeaders.length > 0 && nextSelector < maxDepth) { 25 | data.push( 26 |
  • 27 | {anchor} 28 |
      29 | {this.getLists(section, nextSelector, maxDepth)} 30 |
    31 |
  • 32 | ); 33 | } else { 34 | data.push( 35 |
  • 36 | {anchor} 37 |
  • 38 | ); 39 | } 40 | } 41 | 42 | return data; 43 | }, 44 | 45 | render: function () { 46 | var lists = this.getLists(this.props.contents, 0, this.props.maxDepth); 47 | return ( 48 |
      {lists}
    49 | ); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /public/js/home.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import SplashExample from './components/splash-example'; 4 | 5 | export default function init(state) { 6 | var splashProps = Object.assign({}, state.intl, state.examples.splash); 7 | var containerNode = document.querySelector('.splash-example-container'); 8 | 9 | // Expose React component on its DOM node for testing. 10 | containerNode.component = React.render( 11 | , 12 | containerNode 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /public/js/integration.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import ExampleContainer from './components/example-container'; 4 | 5 | export default function init(state) { 6 | state.examples.forEach(function (example) { 7 | hydrateExample(example.id, { 8 | example: example, 9 | intl : state.intl 10 | }); 11 | }); 12 | } 13 | 14 | function hydrateExample(id, props) { 15 | var exampleNode = document.getElementById(id); 16 | if (!exampleNode) { return; } 17 | 18 | var containerNode = exampleNode.parentNode; 19 | 20 | containerNode.component = React.render( 21 | , 22 | containerNode 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /public/js/main.js: -------------------------------------------------------------------------------- 1 | /* global APP */ 2 | 3 | import homePage from './home'; 4 | import integrationPage from './integration'; 5 | import tableOfContents from './toc'; 6 | 7 | switch (APP.pageType) { 8 | case 'home': 9 | homePage(APP); 10 | break; 11 | 12 | case 'guide': 13 | tableOfContents(2); 14 | break; 15 | 16 | case 'integration': 17 | integrationPage(APP); 18 | break; 19 | } 20 | -------------------------------------------------------------------------------- /public/js/toc.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import TableOfContents from './components/table-of-contents'; 4 | 5 | export default function init(maxDepth) { 6 | var tocNode = document.getElementById('toc'); 7 | if (!tocNode) { 8 | return; 9 | } 10 | 11 | React.render( 12 | , 16 | tocNode 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /public/vendor/highlight/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006, Ivan Sagalaev 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of highlight.js nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY 16 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /public/vendor/highlight/README.md: -------------------------------------------------------------------------------- 1 | # Highlight.js 2 | 3 | [![Build Status](https://travis-ci.org/isagalaev/highlight.js.svg?branch=master)](https://travis-ci.org/isagalaev/highlight.js) 4 | 5 | Highlight.js is a syntax highlighter written in JavaScript. It works in the 6 | browser as well as on the server. It works with pretty much any markup, 7 | doesn't depend on any framework and has automatic language detection. 8 | 9 | 10 | ## Getting Started 11 | 12 | The bare minimum for using highlight.js on a web page is linking to the library 13 | along with one of the styles and calling [`initHighlightingOnLoad`][1]: 14 | 15 | ```html 16 | 17 | 18 | 19 | ``` 20 | 21 | This will find and highlight code inside of `
    ` tags trying to detect
     22 | the language automatically. If automatic detection doesn't work for you, you can
     23 | specify the language in the class attribute:
     24 | 
     25 | ```html
     26 | 
    ...
    27 | ``` 28 | 29 | The list of supported language classes is available in the [class reference][8]. 30 | Classes can also be prefixed with either `language-` or `lang-`. 31 | 32 | To disable highlighting altogether use the `nohighlight` class: 33 | 34 | ```html 35 |
    ...
    36 | ``` 37 | 38 | ## Custom Initialization 39 | 40 | When you need a bit more control over the initialization of 41 | highlight.js, you can use the [`highlightBlock`][2] and [`configure`][3] 42 | functions. This allows you to control *what* to highlight and *when*. 43 | 44 | Here's an equivalent way to calling [`initHighlightingOnLoad`][1] using jQuery: 45 | 46 | ```javascript 47 | $(document).ready(function() { 48 | $('pre code').each(function(i, block) { 49 | hljs.highlightBlock(block); 50 | }); 51 | }); 52 | ``` 53 | 54 | You can use any tags instead of `
    ` to mark up your code. If you don't
     55 | use a container that preserve line breaks you will need to configure
     56 | highlight.js to use the `
    ` tag: 57 | 58 | ```javascript 59 | hljs.configure({useBR: true}); 60 | 61 | $('div.code').each(function(i, block) { 62 | hljs.highlightBlock(block); 63 | }); 64 | ``` 65 | 66 | For other options refer to the documentation for [`configure`][3]. 67 | 68 | 69 | ## Getting the Library 70 | 71 | You can get highlight.js as a hosted or custom-build browser script or as a 72 | server module. Head over to the [download page][4] for all the options. 73 | 74 | Note, that the library is not supposed to work straight from the source on 75 | GitHub, it requires building. If none of the pre-packaged options work for you 76 | refer to the [building documentation][5]. 77 | 78 | 79 | ## License 80 | 81 | Highlight.js is released under the BSD License. See [LICENSE][10] file for 82 | details. 83 | 84 | 85 | ## Links 86 | 87 | The official site for the library is at . 88 | 89 | Further in-depth documentation for the API and other topics is at 90 | . 91 | 92 | Authors and contributors are listed in the [AUTHORS.en.txt][9] file. 93 | 94 | [1]: http://highlightjs.readthedocs.org/en/latest/api.html#inithighlightingonload 95 | [2]: http://highlightjs.readthedocs.org/en/latest/api.html#highlightblock-block 96 | [3]: http://highlightjs.readthedocs.org/en/latest/api.html#configure-options 97 | [4]: https://highlightjs.org/download/ 98 | [5]: http://highlightjs.readthedocs.org/en/latest/building-testing.html 99 | [8]: http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html 100 | [9]: https://github.com/isagalaev/highlight.js/blob/master/AUTHORS.en.txt 101 | [10]: https://github.com/isagalaev/highlight.js/blob/master/LICENSE 102 | -------------------------------------------------------------------------------- /public/vendor/highlight/README.ru.md: -------------------------------------------------------------------------------- 1 | # Highlight.js 2 | 3 | Highlight.js — это подсветчик синтаксиса, написанный на JavaScript. Он работает 4 | и в браузере, и на сервере. Он работает с практически любой HTML разметкой, не 5 | зависит от каких-либо фреймворков и умеет автоматически определять язык. 6 | 7 | 8 | ## Начало работы 9 | 10 | Минимум, что нужно сделать для использования highlight.js на веб-странице — это 11 | подключить библиотеку, CSS-стили и вызывать [`initHighlightingOnLoad`][1]: 12 | 13 | ```html 14 | 15 | 16 | 17 | ``` 18 | 19 | Библиотека найдёт и раскрасит код внутри тегов `
    `, попытавшись
     20 | автоматически определить язык. Когда автоопределение не срабатывает, можно явно
     21 | указать язык в атрибуте class:
     22 | 
     23 | ```html
     24 | 
    ...
    25 | ``` 26 | 27 | Список поддерживаемых классов языков доступен в [справочнике по классам][8]. 28 | Класс также можно предваоить префиксами `language-` или `lang-`. 29 | 30 | Чтобы отключить подсветку для какого-то блока, используйте класс `nohighlight`: 31 | 32 | ```html 33 |
    ...
    34 | ``` 35 | 36 | ## Инициализация вручную 37 | 38 | Чтобы иметь чуть больше контроля за инициализацией подсветки, вы можете 39 | использовать функции [`highlightBlock`][2] и [`configure`][3]. Таким образом 40 | можно управлять тем, *что* подсвечивать и *когда*. 41 | 42 | Вот пример инициализация, эквивалентной вызову [`initHighlightingOnLoad`][1], но 43 | с использованием jQuery: 44 | 45 | ```javascript 46 | $(document).ready(function() { 47 | $('pre code').each(function(i, block) { 48 | hljs.highlightBlock(block); 49 | }); 50 | }); 51 | ``` 52 | 53 | Вы можете использовать любые теги разметки вместо `
    `. Если
     54 | используете контейнер, не сохраняющий переводы строк, вам нужно сказать
     55 | highlight.js использовать для них тег `
    `: 56 | 57 | ```javascript 58 | hljs.configure({useBR: true}); 59 | 60 | $('div.code').each(function(i, block) { 61 | hljs.highlightBlock(block); 62 | }); 63 | ``` 64 | 65 | Другие опции можно найти в документации функции [`configure`][3]. 66 | 67 | 68 | ## Установка библиотеки 69 | 70 | Highlight.js можно использовать в браузере прямо с CDN хостинга или скачать 71 | индивидуальную сборку, а также установив модуль на сервере. На 72 | [страница загрузки][4] подробно описаны все варианты. 73 | 74 | Обратите внимание, что библиотека не предназначена для использования в виде 75 | исходного кода на GitHub, а требует отдельной сборки. Если вам не подходит ни 76 | один из готовых вариантов, читайте [документацию по сборке][5]. 77 | 78 | 79 | ## Лицензия 80 | 81 | Highlight.js распространяется под лицензией BSD. Подробнее читайте файл 82 | [LICENSE][10]. 83 | 84 | 85 | ## Ссылки 86 | 87 | Официальный сайт билиотеки расположен по адресу . 88 | 89 | Более подробная документация по API и другим темам расположена на 90 | . 91 | 92 | Авторы и контрибьютора перечислена в файле [AUTHORS.ru.txt][9] file. 93 | 94 | [1]: http://highlightjs.readthedocs.org/en/latest/api.html#inithighlightingonload 95 | [2]: http://highlightjs.readthedocs.org/en/latest/api.html#highlightblock-block 96 | [3]: http://highlightjs.readthedocs.org/en/latest/api.html#configure-options 97 | [4]: https://highlightjs.org/download/ 98 | [5]: http://highlightjs.readthedocs.org/en/latest/building-testing.html 99 | [8]: http://highlightjs.readthedocs.org/en/latest/css-classes-reference.html 100 | [9]: https://github.com/isagalaev/highlight.js/blob/master/AUTHORS.ru.txt 101 | [10]: https://github.com/isagalaev/highlight.js/blob/master/LICENSE 102 | -------------------------------------------------------------------------------- /routes/about.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (route) { 4 | route.name = 'about'; 5 | route.label = 'About'; 6 | 7 | route.get(function (req, res) { 8 | res.render('about', { 9 | activeMenuItem: route.name 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /routes/dust.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getExamples = require('../lib/examples').get; 4 | var renderExamples = require('../lib/examples').render; 5 | var pkgMeta = require('../lib/package-meta')('dust-intl'); 6 | 7 | module.exports = function (route) { 8 | route.name = 'dust'; 9 | 10 | route.get(function (req, res, next) { 11 | var isProduction = req.app.get('env') === 'production'; 12 | 13 | getExamples('dust', {cache: isProduction}).then(function (examples) { 14 | res.expose(examples, 'examples'); 15 | res.expose('integration', 'pageType'); 16 | 17 | res.render('dust', { 18 | activeMenuItem: route.name, 19 | usesDustIntl : true, 20 | package : pkgMeta, 21 | examples : renderExamples(examples, res.intl) 22 | }); 23 | }).catch(next); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /routes/github.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (route) { 4 | route.name = 'github'; 5 | route.label = 'GitHub'; 6 | 7 | route.get(function (req, res) { 8 | res.render('github', { 9 | activeMenuItem: route.name 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /routes/guides.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | module.exports = function (route) { 6 | route.name = 'guides'; 7 | route.label = 'Guides'; 8 | 9 | route.get(function (req, res) { 10 | var guide = req.params.guide || 'index'; 11 | 12 | res.expose('guide', 'pageType'); 13 | res.render(path.join('guides', guide), { 14 | activeMenuItem: route.name 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /routes/handlebars.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getExamples = require('../lib/examples').get; 4 | var renderExamples = require('../lib/examples').render; 5 | var pkgMeta = require('../lib/package-meta')('handlebars-intl'); 6 | 7 | module.exports = function (route) { 8 | route.name = 'handlebars'; 9 | 10 | route.get(function (req, res, next) { 11 | var isProduction = req.app.get('env') === 'production'; 12 | 13 | getExamples('handlebars', {cache: isProduction}) 14 | .then(function (examples) { 15 | res.expose(examples, 'examples'); 16 | res.expose('integration', 'pageType'); 17 | 18 | res.render('handlebars', { 19 | activeMenuItem : route.name, 20 | usesHandlebarsIntl: true, 21 | package : pkgMeta, 22 | examples : renderExamples(examples, res.intl) 23 | }); 24 | }).catch(next); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /routes/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var renderComponent = require('../lib/component').render; 4 | 5 | module.exports = function (route) { 6 | route.name = 'home'; 7 | route.label = 'Home'; 8 | 9 | route.get(function (req, res) { 10 | var splashExample = { 11 | availableNumPhotos: [0, 1, 3, 1000], 12 | 13 | name : 'Annie', 14 | numPhotos: 1000, 15 | takenDate: Date.now() 16 | }; 17 | 18 | var now = Date.now(); 19 | var lastMonth = new Date(now - (30 * 24 * 60 * 60 * 1000)); 20 | 21 | res.expose('home', 'pageType'); 22 | res.expose(splashExample, 'examples.splash'); 23 | 24 | res.render('home', { 25 | activeMenuItem: route.name, 26 | usesReactIntl : true, 27 | 28 | examples: { 29 | splash: renderComponent('splash-example', 30 | Object.assign({}, res.intl, splashExample) 31 | ) 32 | }, 33 | 34 | now: now, 35 | 36 | // Had to do this since the Intl.js polyfill doesn't seem to work 37 | // correctly when you _just_ want the year or month. I should follow 38 | // a bug. 39 | lastMonth: [ 40 | lastMonth.getFullYear(), 41 | lastMonth.getMonth() + 1, 42 | lastMonth.getDate() 43 | ].join('-') 44 | }); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var utils = require('../lib/utils'); 4 | 5 | exports = module.exports = utils.requireDir(__dirname); 6 | 7 | exports.redirect = redirect; 8 | 9 | // ----------------------------------------------------------------------------- 10 | 11 | function redirect(url, status) { 12 | return function (req, res) { 13 | res.redirect(status || 302, url); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /routes/integrations.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (route) { 4 | route.name = 'integrations'; 5 | route.label = 'Integrations'; 6 | 7 | route.get(function (req, res) { 8 | res.render('integrations', { 9 | activeMenuItem: route.name 10 | }); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /routes/react.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var getExamples = require('../lib/examples').get; 4 | var renderExamples = require('../lib/examples').render; 5 | var pkgMeta = require('../lib/package-meta')('react-intl'); 6 | 7 | module.exports = function (route) { 8 | route.name = 'react'; 9 | 10 | route.get(function (req, res, next) { 11 | var isProduction = req.app.get('env') === 'production'; 12 | 13 | getExamples('react', {cache: isProduction}).then(function (examples) { 14 | res.expose(examples, 'examples'); 15 | res.expose('integration', 'pageType'); 16 | 17 | res.render('react', { 18 | activeMenuItem: route.name, 19 | usesReactIntl : true, 20 | package : pkgMeta, 21 | examples : renderExamples(examples, res.intl) 22 | }); 23 | }).catch(next); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO: Remove this when it is no longer required for Express to work in the 4 | // Manhattan Node.js hosting environment. 5 | process.chdir(__dirname); 6 | 7 | var http = require('http'), 8 | app = require('./app'); 9 | 10 | http.createServer(app).listen(app.get('port'), function () { 11 | console.log('%s server listening on: %d', app.get('name'), app.get('port')); 12 | }); 13 | -------------------------------------------------------------------------------- /shared/components/code-block.jsx: -------------------------------------------------------------------------------- 1 | /* global hljs, React */ 2 | 3 | export default React.createClass({ 4 | displayName: 'CodeBlock', 5 | 6 | propTypes: { 7 | highlight: React.PropTypes.bool, 8 | lang : React.PropTypes.string, 9 | wrap : React.PropTypes.bool 10 | }, 11 | 12 | getDefaultProps: function () { 13 | return {highlight: true}; 14 | }, 15 | 16 | shouldComponentUpdate: function (nextProps) { 17 | // This prevents double syntax highlighting of unchanged content. 18 | return this.props.children !== nextProps.children; 19 | }, 20 | 21 | componentDidUpdate: function () { 22 | if (this.props.highlight) { 23 | hljs.highlightBlock(this.refs.code.getDOMNode()); 24 | } 25 | }, 26 | 27 | render: function () { 28 | var classNames = this.props.wrap ? 'code code-wrap': 'code'; 29 | var lang = this.props.highlight ? this.props.lang : 'nohighlight'; 30 | 31 | return ( 32 |
    33 |                 
    34 |                     {this.props.children}
    35 |                 
    36 |             
    37 | ); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /shared/components/dust-example.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import Example from './example'; 4 | import CodeBlock from './code-block'; 5 | import DustOutput from './dust-output'; 6 | import {Tabs, Tab} from './tabs'; 7 | 8 | export default React.createClass({ 9 | displayName: 'DustExample', 10 | 11 | propTypes: { 12 | id: React.PropTypes.string.isRequired, 13 | 14 | source: React.PropTypes.shape({ 15 | template: React.PropTypes.string.isRequired, 16 | context : React.PropTypes.string.isRequired, 17 | intlData: React.PropTypes.string.isRequired 18 | }).isRequired, 19 | 20 | context : React.PropTypes.object.isRequired, 21 | message : React.PropTypes.string, 22 | formats : React.PropTypes.object, 23 | messages: React.PropTypes.object, 24 | 25 | currentLocale : React.PropTypes.string.isRequired, 26 | availableLocales: React.PropTypes.array.isRequired, 27 | onLocaleChange : React.PropTypes.func.isRequired, 28 | }, 29 | 30 | render: function () { 31 | var props = this.props; 32 | 33 | var tabs = [ 34 | 35 | 36 | {props.source.template} 37 | 38 | , 39 | 40 | 41 | 42 | {props.source.context} 43 | 44 | , 45 | 46 | 47 | 48 | {`context.intl = ${props.source.intlData}; 49 | 50 | dust.render(template, context, function (err, html) { 51 | // Put \`html\` into the DOM or use it otherwise... 52 | });`} 53 | 54 | 55 | ]; 56 | 57 | // Insert a "Message" tab if the example uses an i18n message. 58 | if (props.message) { 59 | tabs.splice(1, 0, 60 | 61 | 62 | {props.message} 63 | 64 | 65 | ); 66 | } 67 | 68 | return ( 69 | {tabs}} 75 | output={ 76 | 83 | } /> 84 | ); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /shared/components/dust-output.jsx: -------------------------------------------------------------------------------- 1 | /* global React, dust */ 2 | 3 | export default React.createClass({ 4 | displayName: 'DustOutput', 5 | 6 | propTypes: { 7 | id: React.PropTypes.string.isRequired, 8 | source : React.PropTypes.string.isRequired, 9 | context: React.PropTypes.object.isRequired, 10 | 11 | locales : React.PropTypes.oneOfType([ 12 | React.PropTypes.string, 13 | React.PropTypes.array, 14 | ]).isRequired, 15 | 16 | formats : React.PropTypes.object, 17 | messages: React.PropTypes.object 18 | }, 19 | 20 | getInitialState: function () { 21 | var tmpl = dust.compile(this.props.source, this.props.id); 22 | dust.loadSource(tmpl); 23 | // The state that we have is the compiled template, but alas this is 24 | // passed through in a side channel (registered inside of dust). 25 | return {}; 26 | }, 27 | 28 | componentWillReceiveProps: function (nextProps) { 29 | var tmpl; 30 | if (nextProps.source !== this.props.source) { 31 | tmpl = dust.compile(nextProps.source, this.props.id); 32 | dust.loadSource(tmpl); 33 | // The state that we have is the compiled template, but alas this is 34 | // passed through in a side channel (registered inside of dust). 35 | this.setState({}); 36 | } 37 | }, 38 | 39 | render: function () { 40 | var context = {}, 41 | html, 42 | nextTick; 43 | 44 | Object.assign(context, this.props.context, { 45 | intl: { 46 | locales : this.props.locales, 47 | formats : this.props.formats, 48 | messages: this.props.messages 49 | } 50 | }); 51 | 52 | // This de-async is hacky, and only works because our example templates 53 | // are simple (don't reference external resources such as partials). 54 | nextTick = dust.nextTick; 55 | dust.nextTick = function(cb) { cb(); }; 56 | dust.render(this.props.id, context, function(err, out) { 57 | dust.nextTick = nextTick; 58 | if (!err && out) { 59 | html = out; 60 | } 61 | }); 62 | 63 | if (html) { 64 | return ( 65 |
    68 | ); 69 | } 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /shared/components/example-container.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import HandlebarsExample from './handlebars-example'; 4 | import ReactExample from './react-example'; 5 | import DustExample from './dust-example'; 6 | 7 | var INTEGRATION_COMPONENTS = { 8 | handlebars: HandlebarsExample, 9 | react : ReactExample, 10 | dust : DustExample 11 | }; 12 | 13 | export default React.createClass({ 14 | displayName: 'ExampleContainer', 15 | 16 | propTypes: { 17 | example: React.PropTypes.shape({ 18 | id : React.PropTypes.string.isRequired, 19 | name: React.PropTypes.string.isRequired, 20 | meta: React.PropTypes.object.isRequired, 21 | 22 | type: React.PropTypes.oneOf( 23 | Object.keys(INTEGRATION_COMPONENTS) 24 | ).isRequired, 25 | 26 | source: React.PropTypes.shape({ 27 | template : React.PropTypes.string, 28 | context : React.PropTypes.string, 29 | component: React.PropTypes.string 30 | }).isRequired, 31 | 32 | // Optional. 33 | getComponent: React.PropTypes.func 34 | }).isRequired, 35 | 36 | intl: React.PropTypes.shape({ 37 | availableLocales: React.PropTypes.array.isRequired, 38 | 39 | locales : React.PropTypes.array.isRequired, 40 | messages: React.PropTypes.object.isRequired 41 | }).isRequired 42 | }, 43 | 44 | getInitialState: function () { 45 | var intl = this.props.intl; 46 | var preferredLocale = intl.locales[0]; 47 | var availableLocales = this.props.intl.availableLocales; 48 | var messageId = this.props.example.meta.messageId; 49 | 50 | // For examples that use messages, limit `availableLocales` to those for 51 | // which there are translations. 52 | if (messageId) { 53 | availableLocales = availableLocales.filter(function (locale) { 54 | return intl.messages[locale].hasOwnProperty(messageId); 55 | }); 56 | } 57 | 58 | // Make sure the user's preferredLocale in available, otherwise default 59 | // to "en-US". 60 | var currentLocale = availableLocales.find(function (locale) { 61 | return locale === preferredLocale; 62 | }); 63 | 64 | return { 65 | currentLocale : currentLocale || 'en-US', 66 | availableLocales: availableLocales 67 | }; 68 | }, 69 | 70 | evalContext: function (contextSource) { 71 | if (contextSource) { 72 | // Why? Oh! Why!? 73 | // Ah yes, because we're normalizing template engines 74 | /*jslint evil: true*/ 75 | return (new Function(contextSource + '\nreturn context;'))(); 76 | /*jslint evil: false*/ 77 | } 78 | 79 | return {}; 80 | }, 81 | 82 | generateIntlData: function () { 83 | var currentLocale = this.state.currentLocale; 84 | var formats = this.props.example.meta.formats; 85 | var messages = this.props.intl.messages[currentLocale]; 86 | var messageId = this.props.example.meta.messageId; 87 | var message = messages[messageId]; 88 | 89 | if (message) { 90 | messages = {}; 91 | messages[messageId] = message; 92 | } else { 93 | messages = null; 94 | } 95 | 96 | var intlData = { 97 | locales: currentLocale 98 | }; 99 | 100 | if (messages) { intlData.messages = messages; } 101 | if (formats) { intlData.formats = formats; } 102 | 103 | return intlData; 104 | }, 105 | 106 | updateLocale: function (newLocale) { 107 | this.setState({currentLocale: newLocale}); 108 | }, 109 | 110 | render: function () { 111 | var props = this.props; 112 | var state = this.state; 113 | 114 | var example = props.example; 115 | var ExampleComponent = INTEGRATION_COMPONENTS[example.type]; 116 | 117 | var source = Object.assign({}, example.source, { 118 | intlData: JSON.stringify(this.generateIntlData(), null, 4) 119 | }); 120 | 121 | var messages = props.intl.messages[state.currentLocale]; 122 | 123 | return ( 124 | 135 | ); 136 | } 137 | }); 138 | -------------------------------------------------------------------------------- /shared/components/example.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import LocaleSelect from './locale-select'; 4 | 5 | export default React.createClass({ 6 | displayName: 'Example', 7 | 8 | propTypes: { 9 | id: React.PropTypes.string.isRequired, 10 | 11 | currentLocale : React.PropTypes.string.isRequired, 12 | availableLocales: React.PropTypes.array.isRequired, 13 | onLocaleChange : React.PropTypes.func.isRequired, 14 | 15 | source: React.PropTypes.element.isRequired, 16 | output: React.PropTypes.element.isRequired, 17 | }, 18 | 19 | render: function () { 20 | var props = this.props; 21 | 22 | return ( 23 |
    24 |
    25 | {props.source} 26 |
    27 | 28 |
    29 |

    Rendered

    30 | 31 | {props.output} 32 | 33 |
    34 | 41 |
    42 |
    43 |
    44 | ); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /shared/components/handlebars-example.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import Example from './example'; 4 | import CodeBlock from './code-block'; 5 | import HandlebarsOutput from './handlebars-output'; 6 | import {Tabs, Tab} from './tabs'; 7 | 8 | export default React.createClass({ 9 | displayName: 'HandlebarsExample', 10 | 11 | propTypes: { 12 | id: React.PropTypes.string.isRequired, 13 | 14 | source: React.PropTypes.shape({ 15 | template: React.PropTypes.string.isRequired, 16 | context : React.PropTypes.string.isRequired, 17 | intlData: React.PropTypes.string.isRequired 18 | }).isRequired, 19 | 20 | context : React.PropTypes.object.isRequired, 21 | message : React.PropTypes.string, 22 | formats : React.PropTypes.object, 23 | messages: React.PropTypes.object, 24 | 25 | currentLocale : React.PropTypes.string.isRequired, 26 | availableLocales: React.PropTypes.array.isRequired, 27 | onLocaleChange : React.PropTypes.func.isRequired, 28 | }, 29 | 30 | render: function () { 31 | var props = this.props; 32 | 33 | var tabs = [ 34 | 35 | 36 | {props.source.template} 37 | 38 | , 39 | 40 | 41 | 42 | {props.source.context} 43 | 44 | , 45 | 46 | 47 | 48 | {`var intlData = ${props.source.intlData}; 49 | 50 | var html = template(context, { 51 | data: {intl: intlData} 52 | });`} 53 | 54 | 55 | ]; 56 | 57 | // Insert a "Message" tab if the example uses an i18n message. 58 | if (props.message) { 59 | tabs.splice(1, 0, 60 | 61 | 62 | {props.message} 63 | 64 | 65 | ); 66 | } 67 | 68 | return ( 69 | {tabs}} 75 | output={ 76 | 82 | } /> 83 | ); 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /shared/components/handlebars-output.jsx: -------------------------------------------------------------------------------- 1 | /* global React, Handlebars */ 2 | 3 | export default React.createClass({ 4 | displayName: 'HandlebarsOutput', 5 | 6 | propTypes: { 7 | source : React.PropTypes.string.isRequired, 8 | context : React.PropTypes.object.isRequired, 9 | 10 | locales : React.PropTypes.oneOfType([ 11 | React.PropTypes.string, 12 | React.PropTypes.array, 13 | ]).isRequired, 14 | 15 | formats : React.PropTypes.object, 16 | messages: React.PropTypes.object 17 | }, 18 | 19 | getInitialState: function () { 20 | return { 21 | template: Handlebars.compile(this.props.source) 22 | }; 23 | }, 24 | 25 | componentWillReceiveProps: function (nextProps) { 26 | if (nextProps.source !== this.props.source) { 27 | this.setState({ 28 | template: Handlebars.compile(nextProps.source) 29 | }); 30 | } 31 | }, 32 | 33 | render: function () { 34 | var html = this.state.template(this.props.context, { 35 | data: { 36 | intl: { 37 | locales : this.props.locales, 38 | formats : this.props.formats, 39 | messages: this.props.messages 40 | } 41 | } 42 | }); 43 | 44 | return ( 45 |
    48 | ); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /shared/components/locale-select.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | export default React.createClass({ 4 | displayName: 'LocaleSelect', 5 | 6 | propTypes: { 7 | availableLocales: React.PropTypes.array.isRequired, 8 | 9 | value : React.PropTypes.string, 10 | onChange: React.PropTypes.func, 11 | 12 | valueLink: React.PropTypes.shape({ 13 | value : React.PropTypes.string.isRequired, 14 | requestChange: React.PropTypes.func.isRequired 15 | }) 16 | }, 17 | 18 | getValueLink: function (props) { 19 | return props.valueLink || { 20 | value : props.value, 21 | requestChange: props.onChange 22 | }; 23 | }, 24 | 25 | handleChange: function (e) { 26 | this.getValueLink(this.props).requestChange(e.target.value); 27 | }, 28 | 29 | render: function () { 30 | var value = this.getValueLink(this.props).value; 31 | 32 | return ( 33 | 41 | ); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /shared/components/react-example.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | import Example from './example'; 4 | import CodeBlock from './code-block'; 5 | import {Tabs, Tab} from './tabs'; 6 | 7 | export default React.createClass({ 8 | displayName: 'ReactExample', 9 | 10 | propTypes: { 11 | id : React.PropTypes.string.isRequired, 12 | component: React.PropTypes.func.isRequired, 13 | 14 | source: React.PropTypes.shape({ 15 | component: React.PropTypes.string.isRequired, 16 | intlData : React.PropTypes.string.isRequired 17 | }).isRequired, 18 | 19 | message : React.PropTypes.string, 20 | formats : React.PropTypes.object, 21 | messages: React.PropTypes.object, 22 | 23 | currentLocale : React.PropTypes.string.isRequired, 24 | availableLocales: React.PropTypes.array.isRequired, 25 | onLocaleChange : React.PropTypes.func.isRequired, 26 | }, 27 | 28 | render: function () { 29 | var props = this.props; 30 | var ExampleComponent = props.component; 31 | 32 | var tabs = [ 33 | 34 | 35 | {props.source.component} 36 | 37 | , 38 | 39 | 40 | 41 | {`var intlData = ${props.source.intlData}; 42 | 43 | React.render( 44 | , 45 | document.getElementById('example') 46 | );`} 47 | 48 | 49 | ]; 50 | 51 | // Insert a "Message" tab if the example uses an i18n message. 52 | if (props.message) { 53 | tabs.splice(1, 0, 54 | 55 | 56 | {props.message} 57 | 58 | 59 | ); 60 | } 61 | 62 | return ( 63 | {tabs}} 69 | output={ 70 |
    71 | 75 |
    76 | } /> 77 | ); 78 | } 79 | }); 80 | -------------------------------------------------------------------------------- /shared/components/splash-example.jsx: -------------------------------------------------------------------------------- 1 | /* global React, ReactIntl */ 2 | 3 | import LocaleSelect from './locale-select'; 4 | 5 | var CSSTransitionGroup = React.addons.CSSTransitionGroup; 6 | 7 | export default React.createClass({ 8 | displayName: 'SplashExample', 9 | 10 | propTypes: { 11 | name : React.PropTypes.string.isRequired, 12 | numPhotos: React.PropTypes.number.isRequired, 13 | takenDate: React.PropTypes.number.isRequired, 14 | 15 | locales : React.PropTypes.oneOfType([ 16 | React.PropTypes.string, 17 | React.PropTypes.array, 18 | ]).isRequired, 19 | 20 | messages: React.PropTypes.object.isRequired, 21 | 22 | availableLocales : React.PropTypes.array.isRequired, 23 | availableNumPhotos: React.PropTypes.array.isRequired 24 | }, 25 | 26 | getInitialState: function () { 27 | var locales = this.props.locales; 28 | 29 | return { 30 | currentLocale : Array.isArray(locales) ? locales[0] : locales, 31 | currentNumPhotos: this.props.numPhotos 32 | }; 33 | }, 34 | 35 | updateLocale: function (newLocale) { 36 | this.setState({currentLocale: newLocale}); 37 | }, 38 | 39 | handleNumPhotosChange: function (e) { 40 | this.setState({currentNumPhotos: parseInt(e.target.value, 10)}); 41 | }, 42 | 43 | render: function () { 44 | // This this ref to be lazy. 45 | var {FormattedMessage} = ReactIntl; 46 | 47 | var currentLocale = this.state.currentLocale; 48 | var photosMessage = this.props.messages[currentLocale].photos; 49 | 50 | var numPhotosOptions = this.props.availableNumPhotos.map(function (num) { 51 | return ; 52 | }); 53 | 54 | return ( 55 |
    56 |

    Example

    57 | 58 |
    59 | 62 | 63 | 70 | 71 |
    72 | 73 |
    74 | 86 | 87 | 97 |
    98 |
    99 | ); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /shared/components/tabs.jsx: -------------------------------------------------------------------------------- 1 | /* global React */ 2 | 3 | export {Tabs, Tab}; 4 | 5 | var Tabs = React.createClass({ 6 | displayName: 'Tabs', 7 | 8 | getInitialState: function () { 9 | var selectedIndex; 10 | 11 | React.Children.forEach(this.props.children, (tab, index) => { 12 | if (tab.props.selected) { 13 | selectedIndex = index; 14 | } 15 | }); 16 | 17 | return { 18 | selectedIndex: selectedIndex || 0 19 | }; 20 | }, 21 | 22 | getTabPanels: function (tabs) { 23 | return React.Children.map(tabs, (tab, index) => { 24 | return React.cloneElement(tab, { 25 | selected: index === this.state.selectedIndex 26 | }); 27 | }); 28 | }, 29 | 30 | getTabListItems: function (tabs) { 31 | return React.Children.map(tabs, (tab, index) => { 32 | var isSelected = index === this.state.selectedIndex; 33 | var className = 'tabs-tab' + (isSelected ? ' is-selected' : ''); 34 | 35 | return ( 36 | 44 | ); 45 | }); 46 | }, 47 | 48 | handleTabClick: function (e) { 49 | var controlsId; 50 | var index; 51 | 52 | if (e.target.getAttribute('role') === 'tab') { 53 | controlsId = e.target.getAttribute('aria-controls'); 54 | 55 | index = this.props.children.findIndex((tab) => { 56 | return tab.props.id === controlsId; 57 | }); 58 | 59 | if (index !== this.state.selectedIndex) { 60 | this.setState({ 61 | selectedIndex: index 62 | }); 63 | } 64 | } 65 | }, 66 | 67 | render: function () { 68 | return ( 69 |
    70 | 73 | 74 | {this.getTabListItems(this.props.children)} 75 | 76 | 77 | {this.getTabPanels(this.props.children)} 78 |
    79 | ); 80 | } 81 | }); 82 | 83 | var Tab = React.createClass({ 84 | displayName: 'Tab', 85 | 86 | propTypes: { 87 | id : React.PropTypes.string.isRequired, 88 | label : React.PropTypes.string.isRequired, 89 | selected: React.PropTypes.bool.isRequired 90 | }, 91 | 92 | getDefaultProps: function () { 93 | return { 94 | selected: false 95 | }; 96 | }, 97 | 98 | render: function () { 99 | return ( 100 | 108 | ); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /tasks/filesize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* jshint -W079 */ 4 | var Promise = global.Promise || require('promise'); 5 | /* jshint +W079 */ 6 | 7 | var path = require('path'), 8 | utils = require('../lib/utils'), 9 | zlib = require('zlib'); 10 | 11 | var gzip = utils.promisify(zlib.gzip); 12 | 13 | function getLength(compressed) { 14 | return compressed.length; 15 | } 16 | 17 | module.exports = function (grunt) { 18 | grunt.registerMultiTask('filesize', 'Calculates the size of gzipped files', function () { 19 | var done = this.async(); 20 | 21 | Promise.all(this.files.map(function(file) { 22 | var src = file.src.filter(function(filepath) { 23 | return grunt.file.exists(path.join(file.cwd, filepath)); 24 | }); 25 | 26 | var sizes = Promise.all(src.map(function(filepath) { 27 | filepath = path.join(file.cwd, filepath); 28 | return gzip(grunt.file.read(filepath)).then(getLength); 29 | })); 30 | 31 | return sizes.then(function (values) { 32 | var result = {}; 33 | 34 | values.forEach(function (value, i) { 35 | result[src[i]] = { 36 | bytes: value, 37 | kbs: Math.round(value * 10 / 1024) / 10 38 | }; 39 | }); 40 | 41 | var output = JSON.stringify(result, null, 4) + '\n'; 42 | 43 | // Write joined contents to destination filepath. 44 | grunt.file.write(file.dest, output); 45 | // Print a success message. 46 | grunt.log.writeln('File "' + file.dest + '" created.'); 47 | 48 | return result; 49 | }); 50 | })).then(function () { 51 | done(); 52 | }, done); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /tests/fixtures/modules/bar.js: -------------------------------------------------------------------------------- 1 | exports.baz = 'baz'; 2 | -------------------------------------------------------------------------------- /tests/fixtures/modules/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = function () {}; 2 | -------------------------------------------------------------------------------- /tests/fixtures/modules/index.js: -------------------------------------------------------------------------------- 1 | exports.bar = 'bar'; 2 | -------------------------------------------------------------------------------- /tests/functional/includes/date.mock.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // Create a date object for a specific instant in time (my 36th birthday...) 5 | 6 | var d = new Date(); 7 | d.setUTCFullYear(2014, 11, 8); 8 | d.setUTCHours(12, 34, 0, 0); 9 | 10 | // Adjust it based on the time zone offset, so that formatted date/time 11 | // will be what we expect, independent of the time zone (minus the time 12 | // zone name, which is a different story) 13 | 14 | d.setUTCMinutes(d.getUTCMinutes() + d.getTimezoneOffset()); 15 | 16 | // Highjack the Date object, and make sure that `new Date()` (without param) 17 | // and `Date.now()` return the time computed above. This will guarantee that 18 | // date/time formatted absolutely does not keep changing, and we can test it 19 | // against a static value. 20 | 21 | var __Date__ = Date; 22 | 23 | Date = function (value) { 24 | return new __Date__(value !== undefined ? value : d.getTime()); 25 | }; 26 | 27 | Date.now = function () { 28 | return d.getTime(); 29 | }; 30 | }()); 31 | -------------------------------------------------------------------------------- /tests/functional/includes/fn.bind.polyfill.js: -------------------------------------------------------------------------------- 1 | // Needed for PhantomJS < 2.0 2 | 3 | if (!Function.prototype.bind) { 4 | var Empty = function () {}; 5 | 6 | Function.prototype.bind = function bind(that) { // .length is 1 7 | var target = this; 8 | 9 | if (typeof target != 'function') { 10 | throw new TypeError('Function.prototype.bind called on incompatible ' + target); 11 | } 12 | 13 | var args = Array.prototype.slice.call(arguments, 1); 14 | 15 | var binder = function() { 16 | if (this instanceof bound) { 17 | var result = target.apply( 18 | this, 19 | args.concat(Array.prototype.slice.call(arguments)) 20 | ); 21 | if (Object(result) === result) { 22 | return result; 23 | } 24 | return this; 25 | } else { 26 | return target.apply( 27 | that, 28 | args.concat(Array.prototype.slice.call(arguments)) 29 | ); 30 | } 31 | }; 32 | 33 | var boundLength = Math.max(0, target.length - args.length); 34 | var boundArgs = []; 35 | 36 | for (var i = 0; i < boundLength; i++) { 37 | boundArgs.push('$' + i); 38 | } 39 | 40 | var bound = Function('binder', 'return function(' + boundArgs.join(',') + '){return binder.apply(this,arguments)}')(binder); 41 | 42 | if (target.prototype) { 43 | Empty.prototype = target.prototype; 44 | bound.prototype = new Empty(); 45 | // Clean up dangling references. 46 | Empty.prototype = null; 47 | } 48 | 49 | return bound; 50 | }; 51 | } -------------------------------------------------------------------------------- /tests/functional/includes/html.event.polyfill.js: -------------------------------------------------------------------------------- 1 | // Needed for PhantomJS < 2.0 2 | 3 | if (!window.Event || typeof Event !== 'function') { 4 | Event = function (type, cfg) { 5 | var evt = document.createEvent('HTMLEvents'); 6 | evt.initEvent(type, cfg.bubbles, cfg.cancelable); 7 | return evt; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /tests/functional/integration-tests.js: -------------------------------------------------------------------------------- 1 | /*globals casper */ 2 | 3 | // Test the various integration pages. 4 | // 5 | // We limit ourselves to one test per integration page. This may seem low, 6 | // but keep in mind that functional tests are not supposed to be as exhaustive 7 | // as unit tests. Instead, their role is to look at the system as a whole. 8 | // Nevertheless, we test a variety of features of FormatJS and multiple locales 9 | // to try and get the highest possible coverage and confidence that the site is 10 | // working properly. 11 | // 12 | // All the tests basically do the same thing. Load a page, modify the value 13 | // of the locale selection combo box, and check the output of the corresponding 14 | // example with a static value. Because these tests are all the same, they are 15 | // parameterized below. 16 | 17 | 'use strict'; 18 | 19 | var testData = { 20 | 'Test React integration example': { 21 | comment: 'Test React integration - Relative time formatting example using ja-JP', 22 | type: 'react', 23 | path: '/react/v1/', 24 | locale: 'ja-JP', 25 | id: 'ex-react-relative', 26 | output_selector: 'li:nth-child(2)', 27 | expected_output: '2 時間前' 28 | }, 29 | 'Test Handlebars integration example': { 30 | comment: 'Test Handlebars integration - Number formatting example using es-AR', 31 | type: 'handlebars', 32 | path: '/handlebars/', 33 | locale: 'fr-FR', 34 | id: 'ex-handlebars-number', 35 | output_selector: 'li:nth-child(3)', 36 | expected_output: '100,95\xA0$US' 37 | }, 38 | 'Test Dust integration example': { 39 | comment: 'Test Dust integration - Time custom formatting example using cs-CZ', 40 | type: 'dust', 41 | path: '/dust/', 42 | locale: 'cs-CZ', 43 | id: 'ex-dust-custom', 44 | output_selector: 'li:nth-child(3)', 45 | expected_output: '12:34' 46 | } 47 | }; 48 | 49 | Object.keys(testData).forEach(function (name) { 50 | var data = testData[name]; 51 | 52 | casper.test.begin(name, function (test) { 53 | 54 | test.comment('Loading page...'); 55 | casper.start(casper.host + data.path, function () { 56 | test.pass('Page was loaded successfully!'); 57 | }); 58 | 59 | casper.then(function () { 60 | casper.test.comment(data.comment); 61 | 62 | casper.evaluate(function (id, locale) { 63 | // Change the value of the locale selection combo box... 64 | var el = document.querySelector('#' + id + ' .locale-select'); 65 | el.value = locale; 66 | 67 | var evt = new Event('change', { bubbles: true }); 68 | el.dispatchEvent(evt); 69 | }, data.id, data.locale); 70 | 71 | // Some tests (example: Ember) run asynchronously... 72 | casper.wait(100, function () { 73 | // Retrieve the example's output... 74 | var output = casper.evaluate(function (selector_output) { 75 | var outputElement = document.querySelector(selector_output); 76 | return outputElement.textContent.trim(); 77 | }, '#' + data.id + ' .' + data.type + '-output ' + (data.output_selector || '')); 78 | 79 | test.assertEquals(output, data.expected_output); 80 | }); 81 | }); 82 | 83 | casper.run(function() { 84 | test.done(); 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/functional/splash-example.js: -------------------------------------------------------------------------------- 1 | /*globals casper */ 2 | 3 | 'use strict'; 4 | 5 | casper.test.begin('Test FormatJS home page splash example', function (test) { 6 | 7 | test.comment('Load home page'); 8 | casper.start(casper.host + '/', function () { 9 | test.pass('Page was loaded'); 10 | }); 11 | 12 | casper.then(function () { 13 | test.comment('Test splash example'); 14 | 15 | test.assertExists('.splash-example-container', 'Found splash example container'); 16 | test.assertExists('.num-photos-select', 'Found number select'); 17 | test.assertExists('.locale-select', 'Found locale select'); 18 | test.assertExists('.splash-example-output', 'Found splash example output'); 19 | 20 | casper.evaluate(function (numPhotos, locale) { 21 | // Start by setting the takenDate prop to a known value so that we 22 | // can compare the example's output to a static value. 23 | var container = document.querySelector('.splash-example-container'); 24 | var component = container.component; 25 | component.setProps({ takenDate: 1423852666565 }); 26 | 27 | // Change the value of the # photos combo box... 28 | var numPhotosSelect = document.querySelector('.num-photos-select'); 29 | numPhotosSelect.value = numPhotos; 30 | 31 | var chgEvt1 = new Event('change', { bubbles: true }); 32 | numPhotosSelect.dispatchEvent(chgEvt1); 33 | 34 | // Change the value of the locale selection combo box... 35 | var localeSelect = document.querySelector('.locale-select'); 36 | localeSelect.value = locale; 37 | 38 | var chgEvt2 = new Event('change', { bubbles: true }); 39 | localeSelect.dispatchEvent(chgEvt2); 40 | }, 3, 'fr-FR'); 41 | 42 | this.wait(1000, function () { 43 | var output = this.evaluate(function () { 44 | // Retrieve the example's output... 45 | var outputElement = document.querySelector('.splash-example-output'); 46 | return outputElement.textContent.trim(); 47 | }); 48 | 49 | test.assertEquals(output, 'Le 13 février 2015, Annie a pris 3 photographies.'); 50 | }); 51 | }); 52 | 53 | casper.run(function () { 54 | test.done(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/functional/utils/casper-setup.js: -------------------------------------------------------------------------------- 1 | /*globals casper */ 2 | 3 | 'use strict'; 4 | 5 | casper.host = 'http://' + casper.cli.options.host; 6 | 7 | casper.on('page.initialized', function () { 8 | casper.page.injectJs('tests/functional/includes/fn.bind.polyfill.js'); 9 | casper.page.injectJs('tests/functional/includes/html.event.polyfill.js'); 10 | casper.page.injectJs('tests/functional/includes/date.mock.js'); 11 | }); 12 | 13 | casper.on('page.error', function (msg, trace) { 14 | this.echo('Error: ' + msg, 'ERROR'); 15 | this.echo('file: ' + trace[0].file, 'WARNING'); 16 | this.echo('line: ' + trace[0].line, 'WARNING'); 17 | this.echo('function: ' + trace[0]['function'], 'WARNING'); 18 | this.die('Aborted due to JavaScript exception'); 19 | }); 20 | 21 | // Log any browser console.log messages to grunt 22 | casper.on('remote.message', function(message) { 23 | this.echo('remote message caught: ' + message); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/health-check.js: -------------------------------------------------------------------------------- 1 | // This file checks that we get a HTTP 200 status code and the appropriate 2 | // content type for all the routes defined in the app. 3 | 4 | 'use strict'; 5 | 6 | /*global describe, it */ 7 | 8 | var path = require('path'); 9 | var request = require('supertest'); 10 | var utils = require('../lib/utils'); 11 | var app = require('../app'); 12 | var routes = utils.requireDir(path.join(__dirname, '../routes/')); 13 | 14 | var opt = process.argv[process.argv.length - 1]; 15 | var m = opt.match(/^--host=(.*)/); 16 | if (!m) { 17 | throw new Error('Missing required --host parameter'); 18 | } 19 | 20 | var host = m[1]; 21 | 22 | console.log('Testing host: ' + host); 23 | 24 | request = request(host); 25 | 26 | describe('Functional tests', function () { 27 | Object.keys(routes) 28 | .map(app.getPathTo) 29 | .forEach(function (routePath) { 30 | it('correctly responds on the ' + routePath + ' route', function (done) { 31 | request.get(routePath) 32 | .expect(200) 33 | .expect('Content-Type', 'text/html; charset=utf-8') 34 | .end(done); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/istanbul-hook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var istanbul = require('istanbul'); 4 | var minimatch = require('minimatch'); 5 | 6 | // Set of files we want Istanbul to _really_ ignore. 7 | var IGNORE_FILES = [ 8 | '**/node_modules/**' 9 | ]; 10 | 11 | function isIgnoredFile(file) { 12 | return IGNORE_FILES.some(function (pattern) { 13 | return minimatch(file, pattern); 14 | }); 15 | } 16 | 17 | // Override Istanbul's `require()` hook, so that certain modules, like 18 | // polyfills, can be _really_ ignored from Istanbul coverage numbers. 19 | var istanbulRequireHook = istanbul.hook.hookRequire; 20 | istanbul.hook.hookRequire = function (matcher, transformer, options) { 21 | istanbulRequireHook(matcher, function (code, filename) { 22 | if (isIgnoredFile(filename)) { 23 | console.log('Ignoring from code coverage: %s', filename); 24 | return code; 25 | } 26 | 27 | return transformer(code, filename); 28 | }, options); 29 | }; 30 | 31 | // Satisfy post-require-hook interface. 32 | module.exports = function () {}; 33 | -------------------------------------------------------------------------------- /tests/unit/lib.component.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var components = require('../../lib/component'); 3 | var expect = require('chai').expect; 4 | 5 | if (!global.React) { 6 | global.React = require('react'); 7 | } 8 | 9 | describe('Component', function () { 10 | describe('require()', function () { 11 | it('returns an object', function () { 12 | expect(components.require('code-block')) 13 | .to.be.a('function'); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/unit/lib.examples.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | if (!global.Promise) { 3 | global.Promise = require('promise'); 4 | } 5 | 6 | var examples = require('../../lib/examples'); 7 | var chai = require('chai'); 8 | var chaiAsPromised = require("chai-as-promised"); 9 | var expect = chai.use(chaiAsPromised).expect; 10 | 11 | describe('Examples', function () { 12 | describe('get()', function () { 13 | it('throws when no type is provided', function () { 14 | expect(function () { 15 | return examples.get(); 16 | }).to.throw(); 17 | }); 18 | it('gets all the Dust examples', function () { 19 | return expect(examples.get('dust')) 20 | .to.eventually.be.an('array'); 21 | }); 22 | it('gets all the Handlebars examples', function () { 23 | return expect(examples.get('handlebars')) 24 | .to.eventually.be.an('array'); 25 | }); 26 | it('throws for unknown example types', function () { 27 | return expect(examples.get('unknown template engine')) 28 | .to.eventually.be.rejected; 29 | }); 30 | it('gets all the React examples', function () { 31 | return expect(examples.get('react', { cache: true })) 32 | .to.eventually.be.an('array'); 33 | }); 34 | it('gets all the cached React examplex', function () { 35 | return expect(examples.get('react', { cache: true })) 36 | .to.eventually.be.an('array'); 37 | }); 38 | }); 39 | describe('render', function () { 40 | it('throws when no examples are provided', function () { 41 | expect(function () { 42 | return examples.render(); 43 | }).to.throw(); 44 | }); 45 | it('throws when no intl object is provided', function () { 46 | expect(function () { 47 | examples.render([]); 48 | }).to.throw(); 49 | expect(function () { 50 | examples.render([], 'hello'); 51 | }).to.throw(); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/unit/lib.helpers.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after*/ 2 | var helpers = require('../../lib/helpers'); 3 | var chai = require('chai'); 4 | var Handlebars = require('handlebars'); 5 | 6 | chai.use(require("chai-as-promised")); 7 | 8 | var expect = chai.expect; 9 | 10 | describe('Helpers', function () { 11 | describe('isEqual', function () { 12 | it('should work as triple equals', function () { 13 | var foo = {}; 14 | var bar = {}; 15 | expect(helpers.isEqual(foo, foo)).to.equal(true); 16 | expect(helpers.isEqual(foo, bar)).to.equal(false); 17 | }); 18 | }); 19 | 20 | describe('setTitle', function () { 21 | it('should update the `title` property', function () { 22 | var obj = {}; 23 | helpers.setTitle.call(obj, 'hello'); 24 | expect(obj).to.have.property('title') 25 | .that.equals('hello'); 26 | }); 27 | }); 28 | 29 | describe('title', function () { 30 | it('returns either the title or the brand', function () { 31 | var obj = { 32 | brand: 'qwer' 33 | }; 34 | expect(helpers.title.call(obj) + '').to.equal(obj.brand); 35 | helpers.setTitle.call(obj, 'hello'); 36 | expect(helpers.title.call(obj) + '').to.equal('hello — qwer'); 37 | }); 38 | }); 39 | 40 | describe('setDescription', function () { 41 | it('should update the `description` property', function () { 42 | var obj = {}; 43 | helpers.setDescription.call(obj, 'hello', 'world', {}); 44 | expect(obj).to.have.property('description') 45 | .that.equals('helloworld'); 46 | }); 47 | }); 48 | 49 | describe('description', function () { 50 | it('returns either the tagline or the description', function () { 51 | var obj = { 52 | tagline: 'cachai' 53 | }; 54 | expect(helpers.description.call(obj)).to.equal(obj.tagline); 55 | helpers.setDescription.call(obj, 'hello', {}); 56 | expect(helpers.description.call(obj)).to.equal('hello'); 57 | }); 58 | }); 59 | 60 | describe('code', function () { 61 | before(function () { 62 | Handlebars.registerHelper('code', helpers.code); 63 | }); 64 | after(function () { 65 | Handlebars.unregisterHelper('code'); 66 | }); 67 | 68 | it('throws when a string is not passed', function () { 69 | expect(function () { 70 | return helpers.code(); 71 | }).to.throw(); 72 | }); 73 | it('wraps inline clode', function () { 74 | expect(helpers.code('asdf', {}) + '').to.equal('asdf'); 75 | }); 76 | it('trims and wraps code blocks', function () { 77 | var template = Handlebars.compile('{{#code "js"}}\n\thello();\n{{/code}}'); 78 | var result = template({}); 79 | 80 | expect(result).to.equal('
    hello();
    '); 81 | }); 82 | it('optionally disables highlight', function () { 83 | var template = Handlebars.compile('{{#code highlight=false}}\n\thello();\n{{/code}}'); 84 | var result = template({}); 85 | 86 | expect(result).to.equal('
    hello();
    '); 87 | }); 88 | it('optionally adds a wrap class', function () { 89 | var template = Handlebars.compile('{{#code "js" wrap=true}}\n\thello();\n{{/code}}'); 90 | var result = template({}); 91 | 92 | expect(result).to.equal('
    hello();
    '); 93 | }); 94 | }); 95 | 96 | describe('npmLink', function () { 97 | it('points to npm correctly', function () { 98 | expect(helpers.npmLink('asdf') + '').to.equal( 99 | 'asdf' 100 | ); 101 | }); 102 | }); 103 | 104 | describe('releaseDownloadUrl', function () { 105 | it('points to github correctly', function () { 106 | expect(helpers.releaseDownloadUrl('dust-intl', '1.0.0')) 107 | .to.equal('https://github.com/yahoo/dust-intl/archive/v1.0.0.tar.gz'); 108 | }); 109 | }); 110 | 111 | describe('size', function () { 112 | it('should throw for unrecognized modules', function () { 113 | expect(function () { 114 | return helpers.size(Math.random()); 115 | }).to.throw(); 116 | }); 117 | 118 | it('should use the values config.libSizes', function () { 119 | var sizes = require('../../config').libSizes; 120 | sizes['fake-foo-bar'] = { 121 | "bytes": 999, 122 | "kbs": 0.98 123 | }; 124 | Object.keys(sizes).forEach(function (module) { 125 | var size = helpers.size(module); 126 | 127 | if (sizes[module].bytes < 1024) { 128 | expect(size).to.equal(sizes[module].bytes + ' bytes'); 129 | } else { 130 | expect(size).to.equal(sizes[module].kbs + ' KB'); 131 | } 132 | }); 133 | }); 134 | }); 135 | 136 | describe('cdnUrl', function () { 137 | it('returns a rawgit url', function () { 138 | var version = require('dust-intl/package.json').version; 139 | 140 | expect(helpers.cdnUrl('dust-intl/')) 141 | .to.equal('https://cdn.rawgit.com/yahoo/dust-intl/v' + version + '//'); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/unit/lib.messages.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | if (!global.Promise) { 3 | global.Promise = require('promise'); 4 | } 5 | 6 | var messages = require('../../lib/messages'); 7 | var chai = require('chai'); 8 | var chaiAsPromised = require("chai-as-promised"); 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var expect = chai.use(chaiAsPromised).expect; 12 | 13 | describe('Messages', function () { 14 | describe('getMessages', function () { 15 | it('returns an object with one key for each language in /i18n', function () { 16 | var languages = fs.readdirSync('./i18n') 17 | .filter(function (filename) { 18 | return path.extname(filename) === '.yaml'; 19 | }) 20 | .map(function (filename) { 21 | return path.basename(filename, '.yaml'); 22 | }); 23 | return expect(messages()) 24 | .to.eventually.have.keys(languages); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/unit/lib.package-meta.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var pkgMeta = require('../../lib/package-meta'); 3 | var expect = require('chai').expect; 4 | 5 | describe('Package Meta', function () { 6 | it('returns the meta information for installed packages', function () { 7 | expect(pkgMeta('dust-intl')) 8 | .to.have.keys([ 9 | 'name', 10 | 'version', 11 | 'description', 12 | 'dist', 13 | 'hasDownload' 14 | ]) 15 | .and.to.have.property('dist') 16 | .that.is.an('object') 17 | .with.keys(['main', 'withLocales', 'localeData']); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/unit/lib.utils.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var utils = require('../../lib/utils'); 5 | var chai = require('chai'); 6 | var chaiAsPromised = require("chai-as-promised"); 7 | var expect = chai.use(chaiAsPromised).expect; 8 | 9 | describe('Utils', function () { 10 | describe('error()', function () { 11 | it('constructs an error object', function () { 12 | expect(utils.error(400)) 13 | .to.be.instanceof(Error) 14 | .and.to.have.property('status') 15 | .that.equals(400); 16 | }); 17 | }); 18 | describe('requireDir', function () { 19 | it('returns an object with a key for each module', function () { 20 | var dirPath = path.resolve('./tests/fixtures/modules/'); 21 | 22 | var files = fs.readdirSync(dirPath) 23 | .filter(function (file) { 24 | return file !== 'index.js' && path.extname(file) === '.js'; 25 | }) 26 | .map(function (file) { 27 | return path.basename(file, '.js'); 28 | }); 29 | 30 | expect(utils.requireDir(dirPath)) 31 | .to.be.an('object') 32 | .with.keys(files); 33 | }); 34 | }); 35 | describe('promisify', function () { 36 | it('returns a function that fulfills promises', function () { 37 | function delay(ms, value, callback) { 38 | setTimeout(function () { 39 | callback(undefined, value); 40 | }, ms); 41 | } 42 | 43 | expect(utils.promisify(delay)).to.be.a('function'); 44 | 45 | var fn = utils.promisify(delay); 46 | 47 | return expect(fn(1, true)) 48 | .to.eventually.equal(true); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/unit/middleware.intl.js: -------------------------------------------------------------------------------- 1 | /*global describe, it*/ 2 | var express = require('express'); 3 | var request = require('supertest'); 4 | var expstate = require('express-state'); 5 | var expect = require('chai').expect; 6 | var intl = require('../../middleware/intl'); 7 | 8 | var app = express(); 9 | expstate.extend(app); 10 | app.use(intl); 11 | 12 | app.get('/', function (req, res) { 13 | res.end(JSON.stringify({ 14 | intl: res.intl, 15 | locals: res.locals 16 | })); 17 | }); 18 | 19 | describe('Intl middleware', function () { 20 | it.skip('exposes an intl object', function (done) { 21 | request(app).get('/') 22 | .expect(200) 23 | .expect(function (res) { 24 | var data = JSON.parse(res.text.trim()); 25 | 26 | expect(data).to.have.property('intl') 27 | .that.is.an('object') 28 | .with.ownProperty('locales') 29 | .and.with.ownProperty('messages'); 30 | expect(data.intl.locales).to.be.an('array'); 31 | expect(data.locals).to.have.property('data') 32 | .that.is.an('object') 33 | .with.property('intl'); 34 | }) 35 | .end(done); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /views/examples/dust/custom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | price : 1400.34, 3 | timestamp: 1390518044403, 4 | now : new Date(), 5 | yesterday: Date.now() - (1000 * 60 * 60 * 24) 6 | }; 7 | -------------------------------------------------------------------------------- /views/examples/dust/custom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | 8 | time: 9 | hhmm: 10 | hour: numeric 11 | minute: numeric 12 | 13 | number: 14 | USD: 15 | style: currency 16 | currency: USD 17 | 18 | relative: 19 | hours: 20 | units: hour 21 | style: numeric 22 | -------------------------------------------------------------------------------- /views/examples/dust/custom/template.dust: -------------------------------------------------------------------------------- 1 |
      2 |
    • {@formatNumber val=price formatName="USD"/}
    • 3 |
    • {@formatDate val=timestamp formatName="short"/}
    • 4 |
    • {@formatTime val=now formatName="hhmm"/}
    • 5 |
    • {@formatRelative val=yesterday formatName="hours"/}
    • 6 |
    7 | -------------------------------------------------------------------------------- /views/examples/dust/date/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/dust/date/template.dust: -------------------------------------------------------------------------------- 1 |

    {@formatDate val=date month="long" day="numeric" year="numeric"/}

    2 | -------------------------------------------------------------------------------- /views/examples/dust/dateCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/dust/dateCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | -------------------------------------------------------------------------------- /views/examples/dust/dateCustom/template.dust: -------------------------------------------------------------------------------- 1 |

    {@formatDate val=date formatName="short"/}

    2 | -------------------------------------------------------------------------------- /views/examples/dust/intl/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/dust/intl/template.dust: -------------------------------------------------------------------------------- 1 |

    2 | {@formatDate val=date day="numeric" month="long"/} 3 | (current locale) 4 |

    5 | 6 | {@intl locales="fr-FR"} 7 |

    8 | {@formatDate val=date day="numeric" month="long"/} 9 | ("fr-FR" locale) 10 |

    11 | {/intl} 12 | -------------------------------------------------------------------------------- /views/examples/dust/message/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | name : 'Annie', 3 | numPhotos: 1000, 4 | takenDate: Date.now() 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/dust/message/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: photos 2 | -------------------------------------------------------------------------------- /views/examples/dust/message/template.dust: -------------------------------------------------------------------------------- 1 |

    2 | {@formatMessage _key="photos" 3 | name=name 4 | numPhotos=numPhotos 5 | takenDate=takenDate/} 6 |

    7 | -------------------------------------------------------------------------------- /views/examples/dust/number/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | num : 42000, 3 | completed: 0.9, 4 | price : 100.95 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/dust/number/template.dust: -------------------------------------------------------------------------------- 1 |
      2 |
    • {@formatNumber val=num/}
    • 3 |
    • {@formatNumber val=completed style="percent"/}
    • 4 |
    • {@formatNumber val=price style="currency" currency="USD"/}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/dust/numberCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | num : 42000, 3 | completed: 0.9, 4 | price : 100.95 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/dust/numberCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | number: 3 | USD: 4 | style: currency 5 | currency: USD 6 | percentage: 7 | style: percent 8 | -------------------------------------------------------------------------------- /views/examples/dust/numberCustom/template.dust: -------------------------------------------------------------------------------- 1 |
      2 |
    • {@formatNumber val=num/}
    • 3 |
    • {@formatNumber val=completed formatName="percentage"/}
    • 4 |
    • {@formatNumber val=price formatName="USD"/}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/dust/relative/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate : Date.now() - (1000 * 60 * 60 * 24), 3 | commentDate: Date.now() - (1000 * 60 * 60 * 2), 4 | meetingDate: Date.now() + (1000 * 60 * 51) 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/dust/relative/template.dust: -------------------------------------------------------------------------------- 1 |
      2 |
    • {@formatRelative val=postDate/}
    • 3 |
    • {@formatRelative val=commentDate/}
    • 4 |
    • {@formatRelative val=meetingDate/}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/dust/relativeCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 24), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 2) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/dust/relativeCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | relative: 3 | hours: 4 | units: hour 5 | style: numeric 6 | -------------------------------------------------------------------------------- /views/examples/dust/relativeCustom/template.dust: -------------------------------------------------------------------------------- 1 |

    2 | {@formatRelative val=postDate/} (best fit)
    3 | {@formatRelative val=postDate formatName="hours"/} (hours, numeric) 4 |

    5 |

    6 | {@formatRelative val=lastTrip/} (best fit)
    7 | {@formatRelative val=lastTrip formatName="hours"/} (hours, numeric) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/dust/relativeStyle/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 24), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 380) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/dust/relativeStyle/template.dust: -------------------------------------------------------------------------------- 1 |

    2 | {@formatRelative val=postDate/} (best fit)
    3 | {@formatRelative val=postDate style="numeric"/} (numeric) 4 |

    5 |

    6 | {@formatRelative val=lastTrip/} (best fit)
    7 | {@formatRelative val=lastTrip style="numeric"/} (numeric) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/dust/relativeUnits/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 22), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 70) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/dust/relativeUnits/template.dust: -------------------------------------------------------------------------------- 1 |

    2 | {@formatRelative val=postDate/} (best fit)
    3 | {@formatRelative val=postDate units="minute"/} (in minutes) 4 |

    5 |

    6 | {@formatRelative val=lastTrip/} (best fit)
    7 | {@formatRelative val=lastTrip units="day"/} (in days) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/dust/time/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/dust/time/template.dust: -------------------------------------------------------------------------------- 1 |

    {@formatTime val=date hour="numeric" minute="numeric" timeZone="UTC"/}

    2 | -------------------------------------------------------------------------------- /views/examples/ember/date/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/ember/date/template.hbs: -------------------------------------------------------------------------------- 1 |

    {{format-date date day="numeric" month="long" year="numeric"}}

    2 | -------------------------------------------------------------------------------- /views/examples/ember/message/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | name : 'Annie', 3 | numPhotos: 1000, 4 | takenDate: Date.now() 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/ember/message/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: photos 2 | -------------------------------------------------------------------------------- /views/examples/ember/message/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{format-message (intl-get "messages.photos") 3 | name=name 4 | numPhotos=numPhotos 5 | takenDate=takenDate}} 6 |

    7 | -------------------------------------------------------------------------------- /views/examples/ember/number/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | num : 42000, 3 | completed: 0.9, 4 | price : 100.95 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/ember/number/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{format-number num}}
    • 3 |
    • {{format-number completed style="percent"}}
    • 4 |
    • {{format-number price style="currency" currency="USD"}}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/ember/relative/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate : Date.now() - (1000 * 60 * 60 * 24), 3 | commentDate: Date.now() - (1000 * 60 * 60 * 2), 4 | meetingDate: Date.now() + (1000 * 60 * 51) 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/ember/relative/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{format-relative postDate}}
    • 3 |
    • {{format-relative commentDate}}
    • 4 |
    • {{format-relative meetingDate}}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/ember/relativeStyle/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 24), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 380) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/ember/relativeStyle/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{format-relative postDate}} (best fit)
    3 | {{format-relative postDate style="numeric"}} (numeric) 4 |

    5 |

    6 | {{format-relative lastTrip}} (best fit)
    7 | {{format-relative lastTrip style="numeric"}} (numeric) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/ember/relativeUnits/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 22), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 70) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/ember/relativeUnits/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{format-relative postDate}} (best fit)
    3 | {{format-relative postDate units="minute"}} (in minutes) 4 |

    5 |

    6 | {{format-relative lastTrip}} (best fit)
    7 | {{format-relative lastTrip units="day"}} (in days) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/handlebars/custom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | price : 1400.34, 3 | timestamp: 1390518044403, 4 | now : new Date(), 5 | yesterday: Date.now() - (1000 * 60 * 60 * 24) 6 | }; 7 | -------------------------------------------------------------------------------- /views/examples/handlebars/custom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | 8 | time: 9 | hhmm: 10 | hour: numeric 11 | minute: numeric 12 | 13 | number: 14 | USD: 15 | style: currency 16 | currency: USD 17 | 18 | relative: 19 | hours: 20 | units: hour 21 | style: numeric 22 | -------------------------------------------------------------------------------- /views/examples/handlebars/custom/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{formatNumber price "USD"}}
    • 3 |
    • {{formatDate timestamp "short"}}
    • 4 |
    • {{formatTime now "hhmm"}}
    • 5 |
    • {{formatRelative yesterday "hours"}}
    • 6 |
    7 | -------------------------------------------------------------------------------- /views/examples/handlebars/date/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/handlebars/date/template.hbs: -------------------------------------------------------------------------------- 1 |

    {{formatDate date day="numeric" month="long" year="numeric"}}

    2 | -------------------------------------------------------------------------------- /views/examples/handlebars/dateCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/handlebars/dateCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | -------------------------------------------------------------------------------- /views/examples/handlebars/dateCustom/template.hbs: -------------------------------------------------------------------------------- 1 |

    {{formatDate date "short"}}

    2 | -------------------------------------------------------------------------------- /views/examples/handlebars/intl/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | date: new Date() 3 | }; 4 | -------------------------------------------------------------------------------- /views/examples/handlebars/intl/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{formatDate date day="numeric" month="long"}} 3 | (current locale) 4 |

    5 | 6 | {{#intl locales="fr-FR"}} 7 |

    8 | {{formatDate date day="numeric" month="long"}} 9 | ("fr-FR" locale) 10 |

    11 | {{/intl}} 12 | -------------------------------------------------------------------------------- /views/examples/handlebars/message/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | name : 'Annie', 3 | numPhotos: 1000, 4 | takenDate: Date.now() 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/handlebars/message/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: photos 2 | -------------------------------------------------------------------------------- /views/examples/handlebars/message/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{formatMessage (intlGet "messages.photos") 3 | name=name 4 | numPhotos=numPhotos 5 | takenDate=takenDate}} 6 |

    7 | -------------------------------------------------------------------------------- /views/examples/handlebars/number/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | num : 42000, 3 | completed: 0.9, 4 | price : 100.95 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/handlebars/number/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{formatNumber num}}
    • 3 |
    • {{formatNumber completed style="percent"}}
    • 4 |
    • {{formatNumber price style="currency" currency="USD"}}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/handlebars/numberCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | num : 42000, 3 | completed: 0.9, 4 | price : 100.95 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/handlebars/numberCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | number: 3 | USD: 4 | style: currency 5 | currency: USD 6 | percentage: 7 | style: percent 8 | -------------------------------------------------------------------------------- /views/examples/handlebars/numberCustom/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{formatNumber num}}
    • 3 |
    • {{formatNumber completed "percentage"}}
    • 4 |
    • {{formatNumber price "USD"}}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/handlebars/relative/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate : Date.now() - (1000 * 60 * 60 * 24), 3 | commentDate: Date.now() - (1000 * 60 * 60 * 2), 4 | meetingDate: Date.now() + (1000 * 60 * 51) 5 | }; 6 | -------------------------------------------------------------------------------- /views/examples/handlebars/relative/template.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • {{formatRelative postDate}}
    • 3 |
    • {{formatRelative commentDate}}
    • 4 |
    • {{formatRelative meetingDate}}
    • 5 |
    6 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeCustom/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 24), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 2) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | relative: 3 | hours: 4 | units: hour 5 | style: numeric 6 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeCustom/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{formatRelative postDate}} (best fit)
    3 | {{formatRelative postDate "hours"}} (hours, numeric) 4 |

    5 |

    6 | {{formatRelative lastTrip}} (best fit)
    7 | {{formatRelative lastTrip "hours"}} (hours, numeric) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeStyle/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 24), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 380) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeStyle/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{formatRelative postDate}} (best fit)
    3 | {{formatRelative postDate style="numeric"}} (numeric) 4 |

    5 |

    6 | {{formatRelative lastTrip}} (best fit)
    7 | {{formatRelative lastTrip style="numeric"}} (numeric) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeUnits/context.js: -------------------------------------------------------------------------------- 1 | var context = { 2 | postDate: Date.now() - (1000 * 60 * 60 * 22), 3 | lastTrip: Date.now() - (1000 * 60 * 60 * 24 * 70) 4 | }; 5 | -------------------------------------------------------------------------------- /views/examples/handlebars/relativeUnits/template.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{formatRelative postDate}} (best fit)
    3 | {{formatRelative postDate units="minute"}} (in minutes) 4 |

    5 |

    6 | {{formatRelative lastTrip}} (best fit)
    7 | {{formatRelative lastTrip units="day"}} (in days) 8 |

    9 | -------------------------------------------------------------------------------- /views/examples/react/custom/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedNumber = ReactIntl.FormattedNumber; 3 | var FormattedDate = ReactIntl.FormattedDate; 4 | var FormattedTime = ReactIntl.FormattedTime; 5 | var FormattedRelative = ReactIntl.FormattedRelative; 6 | 7 | var Component = React.createClass({ 8 | mixins: [IntlMixin], 9 | 10 | render: function () { 11 | var yesterday = Date.now() - (1000 * 60 * 60 * 24); 12 | 13 | return ( 14 |
      15 |
    • 16 |
    • 17 |
    • 18 |
    • 19 |
    20 | ); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /views/examples/react/custom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | 8 | time: 9 | hhmm: 10 | hour: numeric 11 | minute: numeric 12 | 13 | number: 14 | USD: 15 | style: currency 16 | currency: USD 17 | minimumFractionDigits: 2 18 | 19 | relative: 20 | hours: 21 | units: hour 22 | style: numeric 23 | -------------------------------------------------------------------------------- /views/examples/react/date/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedDate = ReactIntl.FormattedDate; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |

    10 | 15 |

    16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /views/examples/react/dateCustom/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedDate = ReactIntl.FormattedDate; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |

    10 | 11 |

    12 | ); 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /views/examples/react/dateCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | date: 3 | short: 4 | day: numeric 5 | month: long 6 | year: numeric 7 | -------------------------------------------------------------------------------- /views/examples/react/message/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedMessage = ReactIntl.FormattedMessage; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |

    10 | 15 |

    16 | ); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /views/examples/react/message/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: photos 2 | -------------------------------------------------------------------------------- /views/examples/react/messageHTML/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedHTMLMessage = ReactIntl.FormattedHTMLMessage; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |

    10 | 13 |

    14 | ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /views/examples/react/messageHTML/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: commentsHTML 2 | -------------------------------------------------------------------------------- /views/examples/react/messageNested/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedMessage = ReactIntl.FormattedMessage; 3 | var FormattedRelative = ReactIntl.FormattedRelative; 4 | 5 | var Component = React.createClass({ 6 | mixins: [IntlMixin], 7 | 8 | render: function () { 9 | var takenDate = 1421881732917; 10 | 11 | return ( 12 |

    13 | Annie} 16 | numPhotos={1000} 17 | takenAgo={ 18 | 21 | } /> 22 |

    23 | ); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /views/examples/react/messageNested/meta.yaml: -------------------------------------------------------------------------------- 1 | messageId: photosNested 2 | -------------------------------------------------------------------------------- /views/examples/react/number/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedNumber = ReactIntl.FormattedNumber; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |
      10 |
    • 11 |
    • 12 |
    • 13 | 17 |
    • 18 |
    19 | ); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /views/examples/react/numberCustom/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedNumber = ReactIntl.FormattedNumber; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | return ( 9 |
      10 |
    • 11 |
    • 12 |
    • 13 |
    14 | ); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /views/examples/react/numberCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | number: 3 | USD: 4 | style: currency 5 | currency: USD 6 | minimumFractionDigits: 2 7 | 8 | percentage: 9 | style: percent 10 | -------------------------------------------------------------------------------- /views/examples/react/relative/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedRelative = ReactIntl.FormattedRelative; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | var postDate = Date.now() - (1000 * 60 * 60 * 24); 9 | var commentDate = Date.now() - (1000 * 60 * 60 * 2); 10 | var meetingDate = Date.now() + (1000 * 60 * 51); 11 | 12 | return ( 13 |
      14 |
    • 15 |
    • 16 |
    • 17 |
    18 | ); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /views/examples/react/relativeCustom/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedRelative = ReactIntl.FormattedRelative; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | var postDate = Date.now() - (1000 * 60 * 60 * 24); 9 | var lastTrip = Date.now() - (1000 * 60 * 60 * 24 * 2); 10 | 11 | return ( 12 |
    13 |

    14 | 15 | (best fit)
    16 | 17 | 18 | (hours, numeric) 19 |

    20 |

    21 | 22 | (best fit)
    23 | 24 | 25 | (hours, numeric) 26 |

    27 |
    28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /views/examples/react/relativeCustom/meta.yaml: -------------------------------------------------------------------------------- 1 | formats: 2 | relative: 3 | hours: 4 | units: hour 5 | style: numeric 6 | -------------------------------------------------------------------------------- /views/examples/react/relativeStyle/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedRelative = ReactIntl.FormattedRelative; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | var postDate = Date.now() - (1000 * 60 * 60 * 24); 9 | var lastTrip = Date.now() - (1000 * 60 * 60 * 24 * 380); 10 | 11 | return ( 12 |
    13 |

    14 | 15 | (best fit)
    16 | 17 | 18 | (numeric) 19 |

    20 |

    21 | 22 | (best fit)
    23 | 24 | 25 | (numeric) 26 |

    27 |
    28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /views/examples/react/relativeUnits/component.jsx: -------------------------------------------------------------------------------- 1 | var IntlMixin = ReactIntl.IntlMixin; 2 | var FormattedRelative = ReactIntl.FormattedRelative; 3 | 4 | var Component = React.createClass({ 5 | mixins: [IntlMixin], 6 | 7 | render: function () { 8 | var postDate = Date.now() - (1000 * 60 * 60 * 22); 9 | var lastTrip = Date.now() - (1000 * 60 * 60 * 24 * 70); 10 | 11 | return ( 12 |
    13 |

    14 | 15 | (best fit)
    16 | 17 | 18 | (in minutes) 19 |

    20 |

    21 | 22 | (best fit)
    23 | 24 | 25 | (in days) 26 |

    27 |
    28 | ); 29 | } 30 | }); 31 | -------------------------------------------------------------------------------- /views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 8 | 9 | 10 | {{> purecss}} 11 | 12 | 13 | 14 | {{> analytics}} 15 | 16 | 17 | 18 | {{{body}}} 19 | 20 | {{> foot}} 21 | 22 | 23 | 24 | {{> polyfills}} 25 | {{> react}} 26 | {{> formatjs}} 27 | {{> syntax-highlighting}} 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /views/pages/about.hbs: -------------------------------------------------------------------------------- 1 | {{setTitle "About"}} 2 | {{setDescription "Authorship, credits, and security info for " brand ", the libraries, and this website."}} 3 | 4 |
    5 | {{> nav}} 6 |
    7 | 8 |
    9 |

    About {{brand}}

    10 | 11 |

    Authorship

    12 | 13 |

    14 | {{brand}} was created by the Yahoo Presentation Technologies (YPT) team, with special thanks to Andy Earnshaw and Norbert Lindenberg. 15 |

    16 | 17 |

    Industry Standards

    18 | 19 |

    20 | {{brand}} builds on the ECMAScript Internationalization API (ECMA-402), uses locale data from the CLDR, and works with the industry standard ICU Message syntax used by professional translators. 21 |

    22 | 23 |

    Credits

    24 | 25 | {{brand}}, the libraries and website, builds on the work of others: 26 | 27 | 50 | 51 |

    Security

    52 | 53 |

    Please report security issues to the Yahoo Bug Bounty Program.

    54 | 55 |
    56 | -------------------------------------------------------------------------------- /views/pages/github.hbs: -------------------------------------------------------------------------------- 1 | {{setTitle "GitHub"}} 2 | {{setDescription "Overview and details on all the open source repositories on GitHub that make up this project."}} 3 | 4 |
    5 | {{> nav}} 6 |
    7 | 8 |
    9 |

    Everything is on GitHub

    10 | 11 |

    12 | All of the packages that make up {{brand}} are open-source and available on GitHub. Instead of having one large repository, there are several smaller repositories; one per package. 13 |

    14 | 15 |

    Integration Libraries

    16 | 17 |

    18 | These are the main packages that web apps will depend on since they contain all of the Core Libraries plus an integration layer with a template or component system. 19 |

    20 | 21 |
    22 |
    23 |

    24 | React Intl 25 |

    26 |
    27 | {{#with "react-intl"}} 28 | {{> npm-badge}} 29 | {{/with}} 30 |
    31 |

    32 | ReactJS mixin for internationalization. 33 |

    34 |
    35 | 36 |
    37 |

    38 | Ember Intl 39 |

    40 |
    41 | {{#with "ember-intl"}} 42 | {{> npm-badge}} 43 | {{/with}} 44 |
    45 |

    46 | Ember toolbox for internationalization. 47 |

    48 |
    49 | 50 |
    51 |

    52 | Handlebars Intl 53 |

    54 |
    55 | {{#with "handlebars-intl"}} 56 | {{> npm-badge}} 57 | {{/with}} 58 |
    59 |

    60 | Handlebars helpers for internationalization. 61 |

    62 |
    63 | 64 |
    65 |

    66 | Dust Intl 67 |

    68 |
    69 | {{#with "dust-intl"}} 70 | {{> npm-badge}} 71 | {{/with}} 72 |
    73 |

    74 | Dust helpers for internationalization. 75 |

    76 |
    77 | 78 |
    79 |

    80 | Polymer Intl 81 |

    82 |
    83 | {{#with "app-localize-behavior"}} 84 | {{> npm-badge}} 85 | {{/with}} 86 |
    87 |

    88 | Polymer helpers for internationalization. 89 |

    90 |
    91 |
    92 | 93 |

    Core Libraries

    94 | 95 |

    96 | This set of libraries makes up the core of the {{brand}} system. These packages provide features beyond what's built into the modern JavaScript runtimes. While the features these packages provide aren't specified in ECMA-402, they were developed on top of ECMAScript, ICU, and CLDR standards in a future-focused way. 97 |

    98 | 99 |
    100 |
    101 |

    102 | Intl MessageFormat 103 |

    104 |
    105 | {{#with "intl-messageformat"}} 106 | {{> npm-badge}} 107 | {{/with}} 108 |
    109 |

    110 | Format a string with placeholders, including plural and select support to create localized messages. 111 |

    112 |
    113 | 114 |
    115 |

    116 | Intl MessageFormat Parser 117 |

    118 |
    119 | {{#with "intl-messageformat-parser"}} 120 | {{> npm-badge}} 121 | {{/with}} 122 |
    123 |

    124 | Parses ICU message strings to an AST that can be used to format the messages for a person's locale. 125 |

    126 |
    127 | 128 |
    129 |

    130 | Intl RelativeFormat 131 |

    132 |
    133 | {{#with "intl-relativeformat"}} 134 | {{> npm-badge}} 135 | {{/with}} 136 |
    137 |

    138 | Formats JavaScript dates to relative time strings. 139 |

    140 |
    141 | 142 |
    143 |

    144 | Intl Format Cache 145 |

    146 |
    147 | {{#with "intl-format-cache"}} 148 | {{> npm-badge}} 149 | {{/with}} 150 |
    151 |

    152 | Produces instances of JavaScript Intl formats, and caches them for reuse. 153 |

    154 |
    155 |
    156 | 157 |

    Polyfills

    158 | 159 |

    160 | These libraries provide polyfills to "patch" the runtime in older JavaScript environments which don't have implementations of the latest ECMAScript standards. 161 |

    162 | 163 |
    164 |
    165 |

    166 | Intl.js 167 |

    168 |
    169 | {{#with "intl"}} 170 | {{> npm-badge}} 171 | {{/with}} 172 |
    173 |

    174 | Compatibility implementation of the ECMAScript Internationalization API (ECMA-402) for JavaScript. Developed by Andy Earnshaw 175 |

    176 |
    177 | 178 |
    179 |

    180 | Intl Locales Supported 181 |

    182 |
    183 | {{#with "intl-locales-supported"}} 184 | {{> npm-badge}} 185 | {{/with}} 186 |
    187 |

    188 | Utility to help you polyfill the Node.js runtime when the Intl APIs are missing, or if the built-in Intl is missing locale data that you need. 189 |

    190 |
    191 |
    192 | 193 |

    Website

    194 | 195 |

    196 | This website itself is open source and uses all parts of the {{brand}} system that it documents! We are always looking for help in making the documentation better, adding new examples, and translating examples. 197 |

    198 | 199 |
    200 |
    201 |

    202 | {{brand}} 203 |

    204 |

    205 | Documentation for client/server internationalization in JavaScript. 206 |

    207 |
    208 |
    209 | 210 |
    211 |

    Filing Issues

    212 | 213 |

    214 | If you wish to file an issue, you can do so on the specific package's GitHub repository. Try to be descriptive when filing issues, as it makes it easier for us to debug. 215 |

    216 | 217 |

    Submitting Pull Requests

    218 | 219 |

    220 | Pull requests are very welcome, but should be within the scope of the project, and follow the repository's code conventions. Before submitting a pull request, it's always good to file an issue, so we can discuss the details of the PR. 221 |

    222 | 223 |

    The Yahoo CLA

    224 | 225 |

    226 | At Yahoo, we have a single Yahoo Open Source Contributor License Agreement that we ask contributors to electronically sign before merging in their Pull Requests. Here's the CLA's human-readable summary: 227 |

    228 |
    229 |

    230 | You are saying that you have the right to give us this code, which is either your own code, or code that your company allows you to publish. You want to give us this code. We may decide to use this code. You are not going to sue people who use this code, because, after all, you are giving it to an open source project! And if you include code that you didn't write, you'll tell us about it by including the open source license to such code in your contribution so we'll know about it. You are not promising that this code works well, or that you will support it, and we're OK with that. 231 |

    232 |
    233 |
    234 |
    235 | -------------------------------------------------------------------------------- /views/pages/guides/basic-i18n.hbs: -------------------------------------------------------------------------------- 1 | {{setTitle "Basic Internationalization Principles"}} 2 | {{setDescription "General information on getting started internationalization web apps client and server in JavaScript."}} 3 | 4 |
    5 | {{> nav}} 6 |
    7 | 8 |
    9 |

    Basic Internationalization Principles

    10 | 11 |
    12 | 13 |
    14 |

    15 | What Is Internationalization and Why Does It Matter? 16 |

    17 | 18 |

    19 | Internationalized software supports the languages and cultural customs of people throughout the world. The Web reaches all parts of the world. Internationalized web apps provide a great user experience for people everywhere. 20 |

    21 | 22 |

    23 | Localized software adapts to a specific language and culture by translating text into the user's language and formatting data in accordance with the user's expectations. An app is typically localized for a small set of locales. 24 |

    25 | 26 |

    27 | The ECMA-402 JavaScript internalization specification has an excellent overview. 28 |

    29 |
    30 | 31 |
    32 |

    33 | Locales: Language and Region 34 |

    35 | 36 |

    37 | A "locale" refers to the lingual and cultural expectations for a region. It is represented using a "locale code" defined in BCP 47. 38 |

    39 | 40 |

    41 | This code is comprised of several parts separated by hyphens ({{code "-"}}). The first part is a short string representing the language. 42 | The second, optional, part is a short string representing the region. Additionally, various extensions and variants can be specified. 43 |

    44 | 45 |

    46 | Typically, web apps are localized to just the language or language-region combination. Examples of such locale codes are: 47 |

    48 | 49 |
      50 |
    • 51 | {{code "en"}} for English 52 |
    • 53 |
    • 54 | {{code "en-US"}} for English as spoken in the United States 55 |
    • 56 |
    • 57 | {{code "en-GB"}} for English as spoken in the United Kingdom 58 |
    • 59 |
    • 60 | {{code "es-AR"}} for Spanish as spoken in Argentina 61 |
    • 62 |
    • 63 | {{code "ar-001"}} for Arabic as spoken throughout the world 64 |
    • 65 |
    • 66 | {{code "ar-AE"}} for Arabic as spoken in United Arab Emirates 67 |
    • 68 |
    69 | 70 |

    71 | Most internationalized apps only support a small list of locales. 72 |

    73 |
    74 | 75 |
    76 |

    77 | Translating Strings 78 |

    79 | 80 |

    81 | You likely have some text in your application that is in a natural language such as English or Japanese. In order to support other locales, you will need to translate these strings. 82 |

    83 | 84 |

    85 | {{brand}} provides a mechanism to let you write the core "software" of your application without special code for different translations. The considerations for each locale are encapsulated in your translated strings and our libraries. 86 |

    87 | 88 | {{#code "js"}} 89 | var messages = { 90 | en: { 91 | GREETING: 'Hello {name}' 92 | }, 93 | fr: { 94 | GREETING: 'Bonjour {name}' 95 | } 96 | }; 97 | {{/code}} 98 | 99 |

    100 | We use the ICU Message syntax which is also used in Java and PHP. 101 |

    102 |
    103 | 104 |
    105 |

    106 | Bundling Translated Strings 107 |

    108 | 109 |

    110 | It is common to organize your translations primarily by locale, because you only need the translations for the user's current locale. Our template and component library integrations are designed to work with the translations for a single locale. If your app is complex, you can further subdivide your translations, such as by page or section of the site. 111 |

    112 |
    113 | 114 |
    115 |

    116 | Structure of Code 117 |

    118 | 119 |

    120 | The actual formatting and presentation of data and translated strings typically takes these steps: 121 |

    122 | 123 |
      124 |
    1. 125 | Determine the user's locale, as described in Runtime Environments guide. 126 |
    2. 127 | 128 |
    3. 129 | Setup one of {{brand}}'s integrations with the following data: 130 |
        131 |
      • 132 | the user's current locale 133 |
      • 134 |
      • 135 | translated strings for that locale 136 |
      • 137 |
      • 138 | optionally, any custom formats 139 |
      • 140 |
      141 |
    4. 142 | 143 |
    5. 144 | Call the template engine, passing the data that needs formatting. 145 |
    6. 146 |
    147 |
    148 | 149 |
    150 |

    {{brand}} Guides

    151 | 152 | {{> guides-list}} 153 |
    154 |
    155 | -------------------------------------------------------------------------------- /views/pages/guides/index.hbs: -------------------------------------------------------------------------------- 1 | {{setTitle "Getting Started"}} 2 | {{setDescription "General information on getting started internationalization web apps client and server in JavaScript."}} 3 | 4 |
    5 | {{> nav}} 6 |
    7 | 8 |
    9 |

    Getting Started

    10 |

    11 | Start with the Guides to Internationalization 12 |

    13 | 14 |

    15 | Internationalizing web apps is an involved and complex task. We created the following guides to document the process: 16 |

    17 | 18 | {{> guides-list}} 19 | 20 |

    {{brand}} Integrations

    21 | 22 |

    23 | We've also integrated internationalization support into a few common template and component libraries. The documentation pages for these integrations is also a good place to start: 24 |

    25 | 26 | {{> integrations-list}} 27 |
    28 | -------------------------------------------------------------------------------- /views/pages/home.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{> nav}} 3 | 4 |
    5 |

    {{tagline}}

    6 | 7 |
    {{{examples.splash}}}
    8 | 9 |

    10 | 11 | Get Started 12 | 13 |

    14 |
    15 |
    16 | 17 |
    18 |
    19 |

    {{brand}} is a set of JavaScript libraries.

    20 | 21 |
    22 |

    23 | {{brand}} is a modular collection of JavaScript libraries for internationalization that are focused on formatting numbers, dates, and strings for displaying to people. It includes a set of core libraries that build on the JavaScript {{code "Intl"}} built-ins and industry-wide i18n standards, plus a set of integrations for common template and component libraries. 24 |

    25 | 26 |
    27 |
    {{brand}} Integrations
    28 |
    {{brand}} Core Libs
    29 |
    Standards
    30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 |
    37 |

    Integrates with other libraries.

    38 | 39 |
    40 | {{> integrations-list}} 41 | 42 |
    43 |

    44 | For most web projects, internationalization happens in the template or view layer, so we've built integrations with common template and component libraries: React, Ember, Handlebars, and Dust. 45 |

    46 | 47 |

    48 | 49 | Learn more about Integrations 50 | 51 |

    52 |
    53 |
    54 |
    55 |
    56 | 57 |
    58 |
    59 |

    Formats numbers, dates, and string messages.

    60 | 61 |
    62 |
    63 |

    Display numbers with separators.

    64 | 65 |

    66 | 1000.95 67 | 68 | 69 | 70 | {{#intl locales="en-US"}} 71 | {{formatNumber 1000.95}} 72 | English 73 | {{/intl}} 74 | 75 | 76 | {{#intl locales="fr-FR"}} 77 | {{formatNumber 1000.95}} 78 | French 79 | {{/intl}} 80 | 81 | 82 |

    83 |
    84 | 85 |
    86 |

    Display dates and times correctly.

    87 | 88 |

    89 | new Date() 90 | 91 | 92 | 93 | {{#intl locales="en-US"}} 94 | {{formatDate now}} 95 | English (US) 96 | {{/intl}} 97 | 98 | 99 | {{#intl locales="fr-FR"}} 100 | {{formatDate now}} 101 | French 102 | {{/intl}} 103 | 104 | 105 |

    106 |
    107 | 108 |
    109 |

    Display dates relative to "now".

    110 | 111 |

    112 | {{#intl locales="en-US"}} 113 | 114 | "{{lastMonth}}" 115 | 116 | 117 | 118 | 119 | {{formatRelative lastMonth}} 120 | 121 | 122 | {{formatRelative lastMonth style="numeric"}} 123 | 124 | 125 | {{formatRelative lastMonth units="day"}} 126 | 127 | 128 | {{/intl}} 129 |

    130 |
    131 | 132 |
    133 |

    Pluralize labels in strings.

    134 | 135 |

    136 | numComments 137 | 138 | 139 | {{#intl locales="en-US"}} 140 | {{formatMessage (intlGet "messages.en-US.comments") numComments=0}} 141 | {{formatMessage (intlGet "messages.en-US.comments") numComments=1}} 142 | {{formatMessage (intlGet "messages.en-US.comments") numComments=1000}} 143 | {{/intl}} 144 | 145 |

    146 |
    147 |
    148 | 149 | {{!-- 150 |

    Modular, and ready to use.

    151 | --}} 152 | 153 |

    Runs in the browser and Node.js.

    154 | 155 |
    156 |

    157 | {{brand}} has been tested in all the major browsers on both desktop and mobile devices. Careful attention has been applied to make sure the libraries work in ES3 browsers all the way down to IE 6. 158 |

    159 | 160 |

    161 | For many web apps rendering happens on the server, so we made sure {{brand}} works perfectly in Node.js. This allows developers to use {{brand}} on both the server and client-side of their apps. 162 |

    163 |
    164 |
    165 |
    166 | 167 |
    168 |
    169 |

    Built on standards.

    170 | 171 |
    172 |

    173 | {{brand}} is aligned with: ECMAScript Internationalization API (ECMA-402), Unicode CLDR, and ICU Message syntax. By building on these industry standards, {{brand}} leverages APIs in modern browsers and works with the message syntax used by professional translators. 174 |

    175 | 176 | 179 |
    180 |
    181 |
    182 | 183 |
    184 |

    185 | 186 | Get Started 187 | 188 |

    189 |
    190 | -------------------------------------------------------------------------------- /views/pages/integrations.hbs: -------------------------------------------------------------------------------- 1 | {{setTitle "Integrations"}} 2 | {{setDescription "Overview of integrations layer with common template and component libraries."}} 3 | 4 |
    5 | {{> nav}} 6 |
    7 | 8 |
    9 |

    Integrations

    10 |

    11 | Start using {{brand}} via one of our integrations: 12 |

    13 | 14 | {{> integrations-list}} 15 | 16 |

    17 | {{brand}} has integrations with common template and component libraries, since that's the place where developers need to format numbers, dates, and string messages for their web app UIs. 18 |

    19 | 20 |

    Using {{brand}} Anywhere

    21 | 22 |

    23 | If your web project isn't using one of the template/component engines with which we have integrations, don't worry; there's also the core Intl libraries with JavaScript APIs. 24 |

    25 | 26 |

    Core JavaScript Intl Libraries

    27 | 28 |

    29 | All of the {{brand}} integrations build and depend on our core JavaScript Intl libraries. This modular approach allows for maximum code reuse, and the ability to use these core Intl APIs directly or build new integrations. 30 |

    31 | 32 |

    Building an Integration

    33 | 34 |

    35 | We're interested in integrations with other popular template/component libraries. At Yahoo, teams use Handlebars, React, and Dust, which is why {{brand}} has integrations for these libraries. If you've built or are interested in building a {{brand}} integration with another library, feel free to reach out to us! 36 |

    37 | 38 |

    39 | The existing {{brand}} integrations are great models for building a new integration. All of them leverage the same dependencies and build process to create a library that runs on the server and client. 40 |

    41 | 42 |

    Principles Behind Integrations

    43 | 44 |
      45 |
    • 46 |

      47 | The integration library should run everywhere the template/component library runs (e.g. Node.js and the browser). 48 |

      49 |
    • 50 |
    • 51 |

      52 | Author library as ES6 Modules, compiled to CommonJS for Node.js, and a single, bundled file with all of its dependencies for the browser. We use the ES6 Module Transpiler. 53 |

      54 |
    • 55 |
    • 56 |

      57 | Build on and use the built-in {{code "Intl"}} JavaScript APIs. 58 |

      59 |
    • 60 |
    • 61 |

      62 | Aggressively cache and reuse format instances using the Intl Format Cache package. 63 |

      64 |
    • 65 |
    • 66 |

      67 | Propagate Intl data through template/component hierarchy to avoid placing burden on user. 68 |

      69 |
    • 70 |
    • 71 |

      72 | Publish integration library on npm. 73 |

      74 |
    • 75 |
    76 |
    77 | -------------------------------------------------------------------------------- /views/partials/analytics.hbs: -------------------------------------------------------------------------------- 1 | {{#if analytics}} 2 | 11 | {{/if}} 12 | -------------------------------------------------------------------------------- /views/partials/example.hbs: -------------------------------------------------------------------------------- 1 |
    {{{this}}}
    2 | -------------------------------------------------------------------------------- /views/partials/foot.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 11 | 12 | 30 |
    31 | -------------------------------------------------------------------------------- /views/partials/formatjs.hbs: -------------------------------------------------------------------------------- 1 | {{#if usesReactIntl}} 2 | 3 | 4 | {{/if}} 5 | 6 | {{#if usesDustIntl}} 7 | 8 | 9 | 10 | 13 | {{/if}} 14 | 15 | {{#if usesHandlebarsIntl}} 16 | 17 | 18 | 19 | 22 | {{/if}} 23 | -------------------------------------------------------------------------------- /views/partials/guides-list.hbs: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /views/partials/integrations-list.hbs: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /views/partials/integrations/features.hbs: -------------------------------------------------------------------------------- 1 |
      2 |
    • 3 | Formats numbers and dates/times, including those in complex messages. 4 |
    • 5 |
    • 6 | Formats relative times (e.g., "3 hours ago"). 7 |
    • 8 |
    • 9 | Formats complex messages, including plural and select arguments using ICU Message syntax. 10 |
    • 11 |
    • 12 | Supports custom formatters for numbers and dates/times. 13 |
    • 14 |
    15 | -------------------------------------------------------------------------------- /views/partials/integrations/load-locale-data-browser.hbs: -------------------------------------------------------------------------------- 1 |

    2 | By default, {{package.name}} ships with the locale data for English built-in to the library's runtime. When you need to format data in another locale, include its data; e.g., for French: 3 |

    4 | 5 | {{#code "html"}} 6 | 7 | {{/code}} 8 | 9 |

    10 | All 150+ languages supported by this library use their root BCP 47 language tag; i.e., the part before the first hyphen (if any). 11 |

    12 | -------------------------------------------------------------------------------- /views/partials/integrations/load-locale-data-node.hbs: -------------------------------------------------------------------------------- 1 |

    2 | In Node.js, the data for all 150+ languages is pre-loaded and does not need to be loaded manually. 3 |

    4 | -------------------------------------------------------------------------------- /views/partials/integrations/note-intl-browser.hbs: -------------------------------------------------------------------------------- 1 |

    2 | Note: Older browsers do not have the built-in {{code "Intl"}} APIs (ECMA-402). Read more on how to patch the browser runtime using a polyfill. 3 |

    4 | -------------------------------------------------------------------------------- /views/partials/integrations/note-intl-node.hbs: -------------------------------------------------------------------------------- 1 |

    2 | Note: Node.js <= 0.10 doesn't have the built-in Intl APIs (ECMA-402). Node.js 0.12 does, but the default build/distribution only supports English. Read more on how to patch Node.js using a polyfill. 3 |

    4 | -------------------------------------------------------------------------------- /views/partials/integrations/package-install.hbs: -------------------------------------------------------------------------------- 1 |
    2 | {{#code highlight=false wrap=true}} 3 | npm install {{package.name}} 4 | {{/code}} 5 |
    6 | -------------------------------------------------------------------------------- /views/partials/integrations/package-meta.hbs: -------------------------------------------------------------------------------- 1 |
      2 | {{#if package.hasDownload}} 3 |
    • 4 | 5 | Download 6 | 7 |
    • 8 | {{/if}} 9 |
    • 10 | v{{package.version}} 11 |
    • 12 |
    • 13 | {{size package.dist.main}} gz 14 |
    • 15 |
    16 | -------------------------------------------------------------------------------- /views/partials/integrations/see-custom-formats.hbs: -------------------------------------------------------------------------------- 1 |

    2 | See the custom formats section for more information. 3 |

    4 | -------------------------------------------------------------------------------- /views/partials/integrations/see-guide.hbs: -------------------------------------------------------------------------------- 1 |

    2 | See the Guide for details on how to write those messages using the ICU Message syntax. 3 |

    4 | -------------------------------------------------------------------------------- /views/partials/nav.hbs: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /views/partials/npm-badge.hbs: -------------------------------------------------------------------------------- 1 | 2 | npm version 3 | 4 | -------------------------------------------------------------------------------- /views/partials/polyfills.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /views/partials/purecss.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /views/partials/react.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/partials/syntax-highlighting.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | --------------------------------------------------------------------------------