├── .gitignore ├── .gitmodules ├── .travis.yml ├── Contributing.md ├── History.md ├── README.md ├── meteor12-react-test ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions ├── meteor12-test.css ├── meteor12-test.html ├── meteor12-test.jsx └── packages │ ├── msgfmt:core │ └── msgfmt:react12 ├── packages ├── core │ ├── History.md │ ├── Makefile │ ├── README.md │ ├── buildPlugin.js │ ├── lib │ │ ├── locale-all.js │ │ ├── msgfmt-client-integrations.js │ │ ├── msgfmt-client.js │ │ ├── msgfmt-server-integrations.js │ │ ├── msgfmt-server.js │ │ ├── msgfmt.js │ │ └── sanitization.js │ ├── mk │ ├── package.js │ ├── smart.json │ ├── tests │ │ ├── integrations │ │ │ └── handlebars-server.handlebars │ │ ├── tests-client.js │ │ ├── tests-server-integrations.js │ │ └── tests-server.js │ └── versions.json ├── core3 │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmignore │ ├── package.json │ └── src │ │ └── index.js ├── extract │ ├── .versions │ ├── CHANGELOG.md │ ├── README.md │ ├── extract-tests.js │ ├── extract.js │ └── package.js ├── react │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmignore │ ├── CHANGELOG.md │ ├── README.md │ ├── meteor │ │ ├── package.js │ │ └── react-tests.js │ ├── package.json │ └── src │ │ └── index.js ├── react12 │ ├── .versions │ ├── CHANGELOG.md │ ├── README.md │ ├── package.js │ ├── react-tests.js │ └── react.jsx ├── ui-dev-only │ ├── .versions │ ├── README.md │ └── package.js └── ui │ ├── .versions │ ├── History.md │ ├── README.md │ ├── lib │ ├── 3rdparty │ │ ├── taboverride.jquery.js │ │ └── taboverride.js │ ├── client.js │ ├── common.js │ ├── server.js │ ├── ui.css │ └── ui.html │ ├── package.js │ └── tests │ ├── tests-client.js │ └── tests-server.js ├── tests-to-migrate.js └── website ├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── cordova-plugins ├── identifier ├── packages ├── platforms ├── release └── versions ├── client └── test.jade ├── deploy ├── docs ├── docs.css ├── docs.html └── docs.js ├── examples ├── examples.css ├── examples.html └── examples.js ├── messageformat.css ├── messageformat.html ├── messageformat.js ├── package.json ├── packages ├── gadicohen:pkgconfig ├── msgfmt_core ├── msgfmt_extract └── msgfmt_ui ├── public └── images │ ├── backdrop.jpg │ └── jumbotron-back.xcf ├── server └── mfAll.js └── survey ├── data.js ├── survey.html └── survey.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | *~ 3 | cli/node_modules 4 | messageformat.sublime-project 5 | messageformat.sublime-workspace 6 | .idea 7 | .npm 8 | #lib 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "msgfmt:core/lib/intl-messageformat"] 2 | path = packages/core/upstream/intl-messageformat 3 | url = https://github.com/yahoo/intl-messageformat 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | env: WORKING_DIR="./website" PACKAGES="msgfmt:core;msgfmt:ui" 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" 7 | 8 | -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to msgfmt 2 | 3 | All packages are under the main repo. To clone the dev environment: 4 | 5 | **Currently all development is on the `v2` branch.** 6 | 7 | ```bash 8 | $ git clone -b v2 https://github.com/gadicc/meteor-messageformat.git 9 | $ cd meteor-messageformat 10 | ``` 11 | 12 | This directory now contains the repo base dir (README, etc), `msgfmt:core`, `msgfmt:ui`, 13 | directories for those packages, and a `website` directory demonstrating basic functionality 14 | (and also the source for messageformat.meteor.com). 15 | 16 | If you intend to submit a **Pull Request** (see below), rather than cloning the base repo, 17 | you should rather fork the repo to your own account and clone that instead (probably using 18 | a `git:` URL scheme). 19 | 20 | ## Test Driven Development 21 | 22 | *Only **Pull Requests with Tests** will be accepted*. 23 | 24 | Make sure all tests are running during development: 25 | 26 | ```bash 27 | git submodule init && git submodule update 28 | cd website 29 | meteor -p 3002 test-packages 30 | ``` 31 | 32 | and open a browser to http://localhost:3002/. 33 | 34 | 1. Identify the bug or planned feature 35 | 1. Add a test that fails without your change 36 | 1. Create your new code 37 | 1. Confirm that your test now passes 38 | 39 | ## Pull Request Requirements 40 | 41 | We greatly appreciate help with this project but we have a few requirements 42 | to ensure smooth development. 43 | 44 | 1. **Keep your PR as simple and concise as possible**. *Do not mix multiple 45 | **unrelated** features/fixes into a single PR.* It is almost always 46 | impossible for us to accept PRs that ignore this rule. To submit multiple PRs, 47 | simply use a different *branch* for each feature. This is common practice 48 | everywhere. e.g. `git checkout -b faster-loads` (see also the GitHub 49 | pull request doc linked below). 50 | 51 | 1. **All PRs should contain a test, that what would have failed before your 52 | change and passes after (see above)**. If there's no existing test that you 53 | can copy, paste and modify to test for your work, and you don't feel 54 | comfortable writing your own, it's ok, but then please state this when 55 | submitting. 56 | 57 | ## Contributing your change (Pull Requests) 58 | 59 | Please see https://help.github.com/articles/using-pull-requests/. 60 | 61 | In brief: 62 | 63 | ```bash 64 | $ git commit -a # please use a clear commit message 65 | $ git push 66 | ``` 67 | 68 | Go to the URL of your fork's repo and click "Merge Pull Request". 69 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | ## See respective History.md files, for: 2 | 3 | * [msgfmt:core](msgfmt:core/History.md) 4 | * [msgfmt:extract](msgfmt:extract/History.md) 5 | * [msgfmt:ui](msgfmt:ui/History.md) 6 | 7 | ## vNEXT (v2.0.0) 8 | 9 | See "Differences from v0" in the README too. 10 | 11 | * Use `msgfmt.setLocale(locale)` to set the locale. If locale does not 12 | exist, we fallback to the lang only (no regional) and then native, e.g. 13 | en_US -> en -> native. 14 | 15 | * Split off the translation UI into a separate package (#29) 16 | * mf:ui is now router agnostic via nicolaslopezj:meteor-router-layer 17 | 18 | * Server now keeps track of connection locales (#83) thanks @lucazulian 19 | 20 | * localStorage is now used to cache: strings and lastUpdatedAt times for 21 | each language, and the user's current locale. 22 | 23 | * msgfmt.locale(), msgfmt.lang(), msgfmt.dir() reactive getters 24 | 25 | * msgfmt.setBodyDir = true (default) will set direction 26 | 27 | 28 | TODO 29 | 30 | * Retrieve languages via separate JSON request, cache it 31 | * Language updates via DDP. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice: Not in active development 2 | 3 | This project is not currently being actively developed. The released v1 4 | still works (to my knowledge) but reached end of life in 2016. I am not 5 | working on any multi-language projects at this time, but may resume work 6 | on this when I do. v2 would be a pure-npm (non-Meteor specific) package. 7 | Thank you everyone everyone for all your many years of support! 8 | 9 | # meteor-messageformat (v2) [![Build Status](https://api.travis-ci.org/gadicc/meteor-messageformat.svg?branch=v2)](https://travis-ci.org/gadicc/meteor-messageformat) 10 | 11 | MessageFormat i18n support, the Meteor way. 12 | 13 | Easy reactive use of complicated strings (gender, plural, etc) with insanely 14 | easy translation into other languages (through a web UI). 15 | 16 | ## Features 17 | 18 | * Super powerful language use via the industry gold standard, MessageFormat 19 | * Super easy translation via automatic string extraction and translation UI 20 | * Super easy integration via automatic locale set on moment, parsley, etc 21 | * Super fast loading, caching, etc. Works with BrowserPolicy, appcache, etc. 22 | * Offline support (including changing of languages while offline) 23 | * Integrates with autoform, momentjs, parsleyvalidator, cmather:handlebars-server 24 | 25 | For full info, docs and examples, see the 26 | [Meteor MessageFormat home page](http://messageformat.meteorapp.com/) 27 | (or install/clone the smart package and run `meteor` in its `website` directory). 28 | For this pre-release, some info on the site is out of date, and all info in the 29 | READMEs will supercede info on the site (for now). 30 | 31 | **See the end of this README for a showcase of sites built with 32 | meteor-messageformat!** 33 | 34 | ## Support for Meteor <= 1.2.1 (End of Life) 35 | 36 | At some point later this year (2016), support for Meteor versions below 1.3 will be dropped. You will continue to be able to use your last installed version of msgfmt - indefinitely - but later updates will rely - in a non-backwards-compatible manner - on featuers provided by Meteor 1.3. This co-incides with MDG's roadmap to deprecate Atmosphere and move all core Meteor packages to npm. 37 | 38 | New versions will still be on Atmosphere for some time to come, since we are heavily coupled to the Meteor build system for a lot of our "magic". But we'd like to start using ES6+ features and gradually prepare the code for a possible generic npm release in the long term. 39 | 40 | ## v2 pre-release 41 | 42 | **THIS IS AN IN-DEVELOPMENT RELEASE. YOU SHOULD NOT BE USING IT UNLESS YOU KNOW 43 | WHAT YOU'RE DOING. SEE THE VERY END OF THIS DOCUMENT FOR SOME MORE HELP**. 44 | 45 | Current versions of each package (requires manual, explicit updates until 46 | the stable release; consider backing up your database before upgrading): 47 | 48 | ``` 49 | meteor add msgfmt:core@2.0.0-preview.23 # 2016-09-05 50 | 51 | # released packages 52 | meteor add msgfmt:extract # 2016-04-01 (v2.0.0) 53 | meteor add msgfmt:ui # 2016-06-22 (v2.0.0) 54 | 55 | # use on of these depending 56 | meteor add msgfmt:react # 2016-03-14 (v2.0.0) - Meteor 1.3+ 57 | meteor add msgfmt:react@2.0.0-meteor12 # 2016-03-14 (v2.0.0) - Meteor 1.2 58 | ``` 59 | 60 | If you don't want the UI translator on production (i.e. no crowd translation), 61 | instead of adding msgfmt:ui, add `msgfmt:ui-dev-only` (no need to specify 62 | version). 63 | 64 | Subpackage READMEs: 65 | [msgfmt:core](https://github.com/gadicc/meteor-messageformat/tree/v2/packages/core#readme) 66 | | 67 | [msgfmt:extract](https://github.com/gadicc/meteor-messageformat/tree/v2/packages/extract#readme) 68 | | 69 | [msgfmt:ui](https://github.com/gadicc/meteor-messageformat/tree/v2/packages/ui#readme) 70 | | 71 | [msgfmt:react](https://github.com/gadicc/meteor-messageformat/tree/v2/packages/react#readme) 72 | 73 | ## Quick Start 74 | 75 | The most common configuration involves: 76 | 77 | ```bash 78 | $ meteor add msgfmt:core msgfmt:extract msgfmt:ui 79 | ``` 80 | 81 | In your common code (for client + server), add: 82 | 83 | ```js 84 | msgfmt.init('en'); 85 | ``` 86 | 87 | where `en` should be your "native" language, i.e. the language all your 88 | strings are in before any translation occurs. You can supply an optional 89 | second argument with a key-value dictionary of configuration values, see 90 | the [docs](http://messageformat.meteorapp.com/docs) for more. 91 | 92 | Setup your strings like this: 93 | 94 | ```handlebars 95 |

{{mf 'heading_welcome' 'Welcome to my Site'}}

96 |

{{mf 'welcome_name' 'Welcome, {NAME}' NAME=getUserName}}

97 | ``` 98 | 99 | For more complicated examples, see the 100 | [examples page](http://messageformat.meteorapp.com//examples). 101 | For more information about different options, see the 102 | [docs](http://messageformat.meteorapp.com/docs). 103 | 104 | To translate your strings, go to `/translate` in your app, available by default 105 | to any registered user. See the [docs](http://messageformat.meteorapp.com/docs) 106 | about custom security policies. 107 | 108 | ## More info 109 | 110 | ### Testing 111 | 112 | Msgfmt requires Meteor's "full application" test mode to work properly with 113 | tests, i.e. `meteor test --full-app`. Particularly, if you're calling 114 | `msgfmt.init('en')` in, say, `lib/config.js` - it's important to understand 115 | that Meteor completely ignores this file in 'regular' test mode. For more 116 | information, please see [issue #242](https://github.com/gadicc/meteor-messageformat/issues/242#issuecomment-298094711). 117 | 118 | ### Optional Settings 119 | 120 | Defaults are shown below. 121 | 122 | ```js 123 | msgfmt.init('en', { 124 | // Send translations for all languages or current language 125 | sendPolicy: "current", 126 | 127 | // Don't invalidate msgfmt.locale() until new language is fully loaded 128 | waitOnLoaded: true, 129 | // Automatically adjust according to the language used 130 | setBodyDir: true, 131 | 132 | // Save setLocale() in Meteor.user().locale, sync to multiple clients 133 | storeUserLocale: true, 134 | 135 | // Use client's localStorage to avoid reloading unchanged translations 136 | useLocalStorage: true // unless sendCompiled: true, 137 | // Send translations to the client pre-compiled 138 | sendCompiled: false // unless browserPolicy disallowUnsafeEval is set 139 | }); 140 | ``` 141 | 142 | ## Cordova 143 | 144 | There's an issue with the inject-initial package under Cordova which causes information to not be properly hooked to the client. To counter this, you may define the locales of the application in the settings file, under the public element. 145 | ```json 146 | { 147 | "public": { 148 | ..., 149 | "msgfmt": { 150 | "native": "en", 151 | "locales": ["en", "fr"] 152 | } 153 | } 154 | } 155 | ``` 156 | 157 | ### Debug logging 158 | 159 | Before init: 160 | 161 | ```js 162 | msgfmt.init('en', { 163 | logLevel: 'debug' // or 'trace' 164 | }) 165 | ``` 166 | 167 | At runtime: 168 | 169 | ```js 170 | Package['jag:pince'].Logger.setLevel('msgfmt', 'debug'); // or 'trace' 171 | ```` 172 | 173 | ### Events 174 | 175 | ```js 176 | msgfmt.on('localeChange', function(locale) { 177 | doSomethingWith(locale); 178 | }); 179 | ``` 180 | 181 | or 182 | 183 | ``` 184 | Tracker.autorun(function() { 185 | doSomethingWith(msgfmt.locale()); 186 | }); 187 | ``` 188 | 189 | ### Integrating with other packages 190 | 191 | The following calls are done automatically if the package exists: 192 | 193 | * `moment.locale()` 194 | * `ParsleyValidator.setLocale()` 195 | 196 | ### Reactivity 197 | 198 | All `{{mf ...}}` strings are reactive and depend on the locale. When 199 | changing locales, all strings on the currently viewed page will update, 200 | without any further action or reloading. 201 | 202 | * `msgfmt.locale()` is a reactive dependency on the current locale. When 203 | calling `setLocale()`, the value might only change when language data is 204 | ready, depending on the value of `msgfmt.waitOnLoaded`. 205 | 206 | * `msgfmt.lang()` is a reactive dependency on the current language. This 207 | is only the language component of the locale, not the dialect / cultural / 208 | regional settings. e.g. locale `en_US` has a lang of `en`. 209 | 210 | * `msgfmt.dir()` is a reactive dependency on the writing direction of the 211 | current language, either `ltr` or `rtl`. By default, 212 | `msgfmt.setBodyDir = true` and we'll change set the `dir` attribute on 213 | your page's `body` tag (which you can leverage with appropriate CSS rules). 214 | 215 | * `msgfmt.loading()` is a reactive dependency which returns the currently 216 | loading locale if `msgfmt.waitOnLoaded = true`, or returns `false` when 217 | everything is loaded. Useful for UI hints to the user. 218 | 219 | ### Differences from v0 220 | 221 | * The main package is now `msgfmt:core`. 222 | 223 | * The translation UI is now a separate package, `msgfmt:ui`. By default, 224 | it's deployed to production too. If you want translation in your dev 225 | environment only, use `msgfmt:ui-dev-only` *instead* (not both). 226 | 227 | * `mf_extract` is no more. Install `msgfmt:extract` and forget about it, 228 | everything is automatic. 229 | 230 | * The main package namespace is now `msgfmt` and not `mfPkg`. However, 231 | `mfPkg` still exists as an alias so no need to change existing code. 232 | 233 | * Use `msgfmt.setLocale(locale)` to set the locale. 234 | 235 | * We now store the client's locale on the server per-connection. This 236 | means that calling `mf()` from inside a `method` or `publish` will 237 | automatically and correctly output the correct language. 238 | 239 | * During initial page load, language data is loaded in parallel with a 2nd 240 | http request. This is cached in localStorage if `msgfmt.useLocalStorage = 241 | true`. On subsequent visits, only new/changed strings are downloaded. 242 | 243 | * Offline support is now official. In the future, we'll bundle the languages 244 | into the client package as part of the Cordova built process, for 100% offline 245 | support without ever needing to connect once. 246 | 247 | * disallowUnsafeEval is now supported. 248 | 249 | * If `msgfmt.storeUserLocale = true` (default), `setLocale()` will also 250 | store the locale in Meteor.user().locale and sync across multiple instances. 251 | 252 | ### Pre release usage 253 | 254 | * Backup your database! (Particularly your mf* collections) 255 | * Save your most recent `mfAll.js` translations 256 | * Delete mfExtract.js and the mf* collections, e.g. `meteor shell` and then: 257 | ``` 258 | > mfPkg.mfStrings.remove({}); 259 | 337 260 | > mfPkg.mfMeta.remove({}); 261 | 23 262 | > mfPkg.mfRevisions.remove({}); 263 | 707 264 | ``` 265 | * Stop meteor. Remove gadicohen:messageformat, add 266 | * msgfmt:core 267 | * msgfmt:ui 268 | * msgfmt:extract 269 | * Run Meteor and check that everything is working. 270 | 271 | ## Underlying Library Change 272 | 273 | In v0 we used @SlexAxton's [MessageFormat.js](https://github.com/SlexAxton/messageformat.js/), 274 | but switched in v2 to the [FormatJS](http://formatjs.io/) project. MessageFormat.js is 275 | more focused as a server side library with precompilation. We initially offered the 276 | precompilation feature as an option (which also solved a longstanding issue with 277 | BrowserPolicy's disallowUnsafeEval), but resulted in much more data needing to be sent 278 | to the client (i.e. slower loading times), and needing to maintain code to handle both 279 | types of sending in different situations. FormatJS was created using some common code 280 | for similar reasons, and 281 | [is now collaborating](https://github.com/yahoo/intl-messageformat/issues/72) with 282 | Alex for shared code on both projects (particularly message parsing). So ultimately, 283 | no change is needed on user strings and FormatJS was a better bit for this type of 284 | project and what we want to offer our users. 285 | 286 | ## Integrations 287 | 288 | ### autoform 289 | 290 | Use Blaze subexpressions or see Wiki. 291 | 292 | ### cmather:handlebars-server 293 | 294 | **example.handlebars** 295 | 296 | ```handlebars 297 | {{mf 'hello' 'Hello there, {NAME}' NAME=NAME LOCALE=LOCALE}} 298 | ``` 299 | 300 | **example.js** 301 | 302 | ```js 303 | var out = Handlebars.templates['example']({ 304 | NAME: 'Chris', 305 | LOCALE: 'en_US' 306 | }); 307 | ``` 308 | 309 | ### momentjs 310 | 311 | Transparent integration. Calls `moment.locale()` on locale change. 312 | 313 | ### Parsley Validator 314 | 315 | Transparent integration. Calls `ParsleyValidator.setLocale()` on locale change. 316 | 317 | ## Sites built with Meteor-MessageFormat 318 | 319 | * [Openki.net](https://sandbox.openki.net/) - Crowd-sourced Education (there's a [sandbox](https://sandbox.openki.net/) too!) - @1u and @sbalmer 320 | * [White Rabbit Express](https://www.whiterabbitexpress.com) - Buy from Japan - @Maxhodges 321 | 322 | A huge thank you to the above sites and authors for your continued faith and 323 | support in meteor-messageformat over the years, all the way from our early days! 324 | Your bug hunting, PRs and vocal support have been critical to this project and 325 | my motivation in maintaining it. Thank you so much! 326 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 3knrvvkivic714o3fuf 8 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | session # Client-side reactive dictionary for your app 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | standard-minifiers # JS/CSS minifiers run for production mode 16 | es5-shim # ECMAScript 5 compatibility for older browsers. 17 | ecmascript # Enable ECMAScript2015+ syntax in app code 18 | 19 | #autopublish # Publish all data to the clients (for prototyping) 20 | #insecure # Allow all DB writes from clients (for prototyping) 21 | 22 | react 23 | msgfmt:core 24 | msgfmt:react -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /meteor12-react-test/.meteor/versions: -------------------------------------------------------------------------------- 1 | amplify@1.0.0 2 | autoupdate@1.2.4 3 | babel-compiler@5.8.24_1 4 | babel-runtime@0.1.4 5 | base64@1.0.4 6 | binary-heap@1.0.4 7 | blaze@2.1.3 8 | blaze-html-templates@1.0.1 9 | blaze-tools@1.0.4 10 | boilerplate-generator@1.0.4 11 | caching-compiler@1.0.0 12 | caching-html-compiler@1.0.2 13 | callback-hook@1.0.4 14 | check@1.1.0 15 | coffeescript@1.0.11 16 | cosmos:browserify@0.9.4 17 | ddp@1.2.2 18 | ddp-client@1.2.1 19 | ddp-common@1.2.2 20 | ddp-server@1.2.2 21 | deps@1.0.9 22 | diff-sequence@1.0.1 23 | djedi:sanitize-html@1.11.2 24 | djedi:sanitize-html-client@1.11.2 25 | ecmascript@0.1.6 26 | ecmascript-runtime@0.2.6 27 | ejson@1.0.7 28 | es5-shim@4.1.14 29 | fastclick@1.0.7 30 | geojson-utils@1.0.4 31 | hot-code-push@1.0.0 32 | html-tools@1.0.5 33 | htmljs@1.0.5 34 | http@1.1.1 35 | id-map@1.0.4 36 | jag:pince@0.0.6 37 | jquery@1.11.4 38 | jsx@0.2.3 39 | launch-screen@1.0.4 40 | livedata@1.0.15 41 | logging@1.0.8 42 | meteor@1.1.10 43 | meteor-base@1.0.1 44 | meteorhacks:inject-initial@1.0.3 45 | minifiers@1.1.7 46 | minimongo@1.0.10 47 | mobile-experience@1.0.1 48 | mobile-status-bar@1.0.6 49 | mongo@1.1.3 50 | mongo-id@1.0.1 51 | msgfmt:core@2.0.0-preview.19 52 | msgfmt:react@2.0.0-meteor12 53 | npm-mongo@1.4.39_1 54 | observe-sequence@1.0.7 55 | ordered-dict@1.0.4 56 | promise@0.5.1 57 | raix:eventemitter@0.1.3 58 | random@1.0.5 59 | react@0.14.3 60 | react-meteor-data@0.2.4 61 | react-runtime@0.14.4 62 | react-runtime-dev@0.14.4 63 | react-runtime-prod@0.14.4 64 | reactive-dict@1.1.3 65 | reactive-var@1.0.6 66 | reload@1.1.4 67 | retry@1.0.4 68 | routepolicy@1.0.6 69 | session@1.1.1 70 | spacebars@1.0.7 71 | spacebars-compiler@1.0.7 72 | standard-minifiers@1.0.2 73 | templating@1.1.5 74 | templating-tools@1.0.0 75 | tracker@1.0.9 76 | ui@1.0.8 77 | underscore@1.0.4 78 | url@1.0.5 79 | webapp@1.2.3 80 | webapp-hashing@1.0.5 81 | -------------------------------------------------------------------------------- /meteor12-react-test/meteor12-test.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | -------------------------------------------------------------------------------- /meteor12-react-test/meteor12-test.html: -------------------------------------------------------------------------------- 1 | 2 | meteor12-test 3 | 4 | 5 | 6 |
7 | 8 | -------------------------------------------------------------------------------- /meteor12-react-test/meteor12-test.jsx: -------------------------------------------------------------------------------- 1 | if (Meteor.isClient) { 2 | // This code is executed on the client only 3 | 4 | Meteor.startup(function () { 5 | // Use Meteor.startup to render the component after the page is ready 6 | ReactDOM.render(, document.getElementById("render-target")); 7 | }); 8 | } 9 | 10 | App = React.createClass({ 11 | render() { 12 | return ( 13 | {` 14 | {COUNT, number} {COUNT, plural, one { widget } other { widgets }} 15 | `} 16 | ); 17 | } 18 | }); 19 | 20 | msgfmt.init('en'); -------------------------------------------------------------------------------- /meteor12-react-test/packages/msgfmt:core: -------------------------------------------------------------------------------- 1 | ../../msgfmt:core -------------------------------------------------------------------------------- /meteor12-react-test/packages/msgfmt:react12: -------------------------------------------------------------------------------- 1 | ../../msgfmt:react12 -------------------------------------------------------------------------------- /packages/core/History.md: -------------------------------------------------------------------------------- 1 | ## vNEXT 2 | 3 | ## v2.0.0-preview.24 (2017-05-04) 4 | 5 | ### Added 6 | 7 | * Allow `mf('key', 'text', params)` in addition to the regular 8 | `mf('key', params, 'text')` Previously, `mf('key', 'text')` was accepted 9 | too, but there was no way to provide `params` in this ordering. Now if 10 | the second argument is a String, the second and third arguments are swapped. 11 | (#253). 12 | 13 | ## v2.0.0-preview.23 (2016-09-05) 14 | 15 | ### Changed 16 | 17 | * "Broken" translations (that can't be compiled) are no longer displayed as 18 | `[Object object]`; instead the native string will be shown and an error 19 | logged on the console. Thanks, @sbalmer! (#244) 20 | 21 | ## v2.0.0-preview.22 (2016-05-31) 22 | 23 | ### Fixed 24 | 25 | * Don't block publication ready status, fixes spiderable issues. 26 | (thanks @sbalmer) (#231) 27 | 28 | ## v2.0.0-preview.21 (2016-04-13) 29 | 30 | ## Added 31 | 32 | * Polyfill for Intl on older browsers (#163) 33 | 34 | ## Fixed 35 | 36 | * Improved import of mfAll.js from < core.19 (incl v0) 37 | 38 | ## v2.0.0-preview.20 39 | 40 | * Use https to retrieve strings if the current page was loaded with https 41 | (#213) 42 | 43 | ## v2.0.0-preview.19 44 | 45 | * Bugfix: Mark all translations as fuzzy (and not just first match), 46 | when a native key is updated (thanks, @sbalmer) (#209) 47 | 48 | * Possible bugfix: Correctly scope `lang` in syncAll (thanks, @sbalmer) (#209) 49 | 50 | * Possible bugfix: No longer run syncAll in a Fiber, suspected possible cause 51 | of mismatched strings (text being assigned to wrong keys), (#195). 52 | 53 | * Internal bugfix: observeFrom(time, native/trans) works the old way again, 54 | and is used appropriately for performance gains. 55 | 56 | * Enhancement: Skip `extracts.msgfmt~` and `mfAll.js` if the database is 57 | already up to date. 58 | 59 | * Refactor syncAll(), addNative(), langUpdate() methods and handling of mfMeta 60 | data. 61 | 62 | * Enhancement: native text from mfAll.js is no longer ignored if it's more 63 | up to date than database / extracts (affects those that don't use extracts 64 | and like to build mfAll.js by hand). 65 | 66 | ## 2.0.0-preview.18 67 | 68 | * Feature: `cmather:handlebars-server` integration (see README, #32). 69 | * Bugfix: Due to a weird publish error, the Cordova code below 70 | was not included in core.17 (#205) 71 | 72 | ## 2.0.0-preview.17 73 | 74 | * Workaround for Cordova problems, see README. 75 | Many thanks @MartinFournier! (#191) 76 | 77 | * Note: this was released the same time as preview.16 so be aware of below. 78 | TESTED INTERNAL CHANGE but backup your database (mf* database) to be safe. 79 | 80 | ## 2.0.0-preview.16 81 | 82 | * TESTED INTERNAL CHANGE but backup your database (mf* database) to be safe. 83 | 84 | * Don't rely on `_id` in translation data. Set compound index in mongo 85 | on (key, lang). Remove possible duplicates in database when upgrading. 86 | 87 | ## 2.0.0-preview.15 88 | 89 | * Run `syncAll` in it's own Fiber to avoid blocking on load 90 | from `server/mfAll.js` in the case of slow I/O (#175). 91 | Thanks also @sbalmer and @1u. 92 | 93 | * When warning about calling mf() on the server from outside 94 | a method/publish, mention the offfending key (!) 95 | use `log.warn` instead of `console.log`. 96 | Thanks @MartinFournier (#182). 97 | 98 | ## 2.0.0-preview.14 99 | 100 | * Add `check` package (now that Meteor 1.2 is stricter) 101 | * Throw error if `gadicohen:messageformat` (i.e. `v0`) is also installed. 102 | 103 | ## 2.0.0-preview.13 104 | 105 | * JSX support! (#170, #138; thanks @flipace) 106 | 107 | ## v0.0.48 108 | 109 | * Work around /translate/mfAll.js not loading correctly with iron-router (#81) 110 | * mf_extract_wrapper.js@0.0.7 -- fixes some path issues (#77) 111 | * Prevent a moment warning (#79; thanks @maxnowack) 112 | 113 | ## v0.0.47 114 | 115 | * Bump iron-router to 1.0.0, work with that release, and make it a weak dep 116 | 117 | ## v0.0.44 / v0.0.45 118 | 119 | * Support for mf_extract when package installed from 0.9 package server 120 | 121 | * Upgrade deps 122 | ** gadicohen:inject-initial -> meteorhacks:inject-initial 123 | ** gadicohen:headers@0.0.25 (which also has above namespace change) 124 | 125 | ## v0.0.43 126 | 127 | * Security fix, optimization (#64) 128 | 129 | ## v0.0.42 130 | 131 | * Critical security bug fixed (deny function were not enforced) 132 | 133 | ## v0.0.41 134 | 135 | * fix compat with 0.8.3 136 | * initial but broken laika 0.9 support 137 | * fix for tests 138 | 139 | ## v0.0.40 140 | 141 | * Fixes for breaking changes in private Blaze API in Meteor 0.9.1 142 | 143 | ## v0.0.39 144 | 145 | * More fixes for pre-0.9.0 146 | 147 | ## v0.0.38 148 | 149 | * Fix for pre-0.9.0 150 | 151 | ## v0.0.37 152 | 153 | * CoffeeScript support! thanks @betapi (#53) 154 | 155 | ## v0.0.36 156 | 157 | * 0.9.0 rc compatibility, thanks again @tarang (#52) 158 | 159 | ## v0.0.35 160 | 161 | * Fixes for 0.8.3, thanks @tarang (fixes #49) 162 | 163 | ## v0.0.34 164 | 165 | * Removed obsolete {{#isolate}} that threw an exception in 0.8.3 (#49) 166 | @DSpeichert, @matteosaporiti 167 | 168 | ## v0.0.33 169 | 170 | * `params` is now optional in `mf()`. Fixes inconsistency in #47. 171 | * Remove `inject-initial` from smart.json since we ask for `headers` which includes it. 172 | This avoids needing to update both packages each time until Meteor 0.9 is released. 173 | 174 | ## v0.0.30 175 | 176 | * mf_extract: parse function calls with no spaces (fixes #42). thanks @matteosaporiti 177 | 178 | ## v0.0.29 179 | 180 | * 0.8.0+ only, iron-router 0.7.0+ identifiers (fixes #39) 181 | Related note: webUI to be moved out to separate package soon 182 | 183 | ## v0.0.26 184 | 185 | * faster loading with inject-initial, appcache fix for headers 186 | * mf_extract: warn on duplicate keys (fixes #31) 187 | * mf_extract. use tpl/func name of 'unknown' regexp fails (fixes #33) 188 | * server side mf should work even without mfExtract.js (fixes #34) 189 | 190 | ## v0.0.25 191 | 192 | * blaze blockhelper fix (breaks rc0, works on rc1+) 193 | 194 | ## v0.0.24 195 | 196 | * Remove iron-router from smart.json. See (#29) 197 | 198 | ## v0.0.23 199 | 200 | * Fixed no tabOverride() outside of example website (#22) 201 | * Now ignores anything in `packages` directory (#21) 202 | * Various browser compatibilitiy fixes for translation UI (#20) - 203 | thanks lalomartins! 204 | * Allow `mfInit('en')` with no {options}. 205 | * mf_extract now requires underscore from local install, like walk 206 | 207 | * what happened to 0.0.22 and 0.0.21? :) includes some of the above. 208 | 209 | ## v0.0.20 210 | 211 | * Merged Meteor UI support from shark branch, updated to work with 212 | Meteor shark branch (block helpers and raw HTML are working now), 213 | and will also work correctly with spark. *Any changes to 214 | `headers-client.js` should be tested on both the `website` and 215 | `websiteUI` directories.* Note, iron-router for UI is not stable 216 | yet. 217 | 218 | * Updated `meteor-headers` dep to v0.0.15 219 | 220 | ## v0.0.19 221 | 222 | * Add `walk` as a NPM dependency for the *smart package*, so 223 | when `mf_extract_wrapper` calls `mf_extract.js`, it will be 224 | available in the packages directory (#14). 225 | 226 | * Correctly compile text on the server (#12) 227 | 228 | * Remove unnecessary debug console.log's (#13) 229 | 230 | ## v0.0.18 231 | 232 | * mf_extract: Fix JS regexp to correctly identify mf() calls in 233 | Javascript. Adjust docs/docs.js to give correct code for example 234 | parsing. 235 | 236 | * mf_extract: Calling `mf_extract -v` will give more verbose 237 | output. The current version, project dir, and each parsed string. 238 | -------------------------------------------------------------------------------- /packages/core/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | ./run_tests.sh 3 | .PHONY: test 4 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # msgfmt:core 2 | 3 | MessageFormat i18n support, the Meteor way. 4 | 5 | Easy reactive use of complicated strings (gender, plural, etc) with insanely 6 | easy translation into other languages (through a web UI). 7 | 8 | Please read the [general README](../../README.md) first before reading 9 | this `msgfmt:core` README. 10 | 11 | ## Intro 12 | 13 | For full info, docs and examples, see the 14 | [Meteor MessageFormat home page](http://messageformat.meteor.com/) 15 | (or install/clone the smart package and run `meteor` in its `website` directory). 16 | 17 | In short, you'll get to use the `{{mf}}` helper. Simple example: 18 | 19 | ```html 20 |

{{mf 'trans_string' 'This string is translatable'}}

21 | ``` 22 | 23 | but much more complex strings are possible, and are useful even if your 24 | website is only available in one language, e.g.: 25 | 26 | ```html 27 | {{#mf KEY='gender_plural' GENDER=getGender NUM_RESULTS=getNum NUM_CATS=getNum2}} 28 | {GENDER, select, 29 | male {He} 30 | female {She} 31 | other {They} 32 | } found {NUM_RESULTS, plural, 33 | =0 {no results} 34 | one {1 result} 35 | other {# results} 36 | } in {NUM_CATS, plural, 37 | one {1 category} 38 | other {# categories} 39 | }. 40 | {{/mf}} 41 | ``` 42 | 43 | Possible outputs: 44 | 45 | ```html 46 | He found 2 results in 1 category. 47 | She found 1 result in 2 categories. 48 | etc 49 | ``` 50 | 51 | Besides gender, there is support for offsets too, e.g.: 52 | 53 | ```html 54 | You and one other person added this to their profile. 55 | ``` 56 | 57 | For full info, docs and examples, see the 58 | [Meteor MessageFormat home page](http://messageformat.meteor.com/) 59 | (or install/clone the smart package and run `mrt` in its `website` directory). 60 | 61 | ## Initial loading 62 | 63 | * The initial HTML page is injected with lastUpdate times for all locales, 64 | and best locale match for user's client based on `accept-language` header. 65 | 66 | * During msgfmt:core loading (early in the main minified script), we use 67 | any previously saved locale (in localStorage) or otherwise the headerLocale, 68 | compare the lastSync time (if cached previously) to what's available now, 69 | and if there is new data available, we initiate a parallel HTTP request to 70 | fetch it. 71 | 72 | * If unsafe-eval is disallowed, we retrieve all the scripts precompiled from 73 | the server. 74 | 75 | ## Offline Support 76 | 77 | TODO, manifest, buildcompiler, cordova 78 | See current workaround in main README. 79 | 80 | ## Intl polyfill 81 | 82 | In v2 we handle date/number/currency formatting, but it relies on native 83 | browser support. We rely on the 84 | [Intl polyfill](https://github.com/andyearnshaw/Intl.js) 85 | served via [cdn.polyfill.io](https://cdn.polyfill.io/) to provide this 86 | support in earlier browsers, requesting all the locales you currently 87 | offer. (No, we don't lazy load locales for the polyfill) 88 | 89 | This results in an extra external request to the CDN on each initial 90 | page load. polyfill.io is served from the fastly world-wide CDN, and 91 | looks at the browser's user-agent to only send the polyfill if needed. 92 | However, if the extra request bothers you, you can turn it off with: 93 | 94 | ```js 95 | msgfmt.init('en', { 96 | disableIntlPolyfill: true 97 | }); 98 | ``` 99 | -------------------------------------------------------------------------------- /packages/core/buildPlugin.js: -------------------------------------------------------------------------------- 1 | // currently this file only handles extracts, we could do other stuff in the futre 2 | 3 | var fs = Npm.require('fs'); 4 | var EXTRACTS_FILE = 'server/extracts.msgfmt~'; 5 | 6 | /* 7 | * So what's going on here? msgfmt:extracts creates a file with a 8 | * tilde ("~") suffix, to prevent Meteor from reloading when it's updated. 9 | * That file itself is only created on reload, so that would cause a double 10 | * reload and be a total pain. But, Meteor doesn't bundle ~ files either. 11 | * So we use a build plugin, on the presence of a similarly named file, 12 | * to look for this file, and bundle it. This happens on load, so will 13 | * catch it only on the next reload, but that's fine, since the file is 14 | * only used in production. 15 | */ 16 | 17 | function msgfmtHandler(compileStep) { 18 | if (fs.existsSync(EXTRACTS_FILE)) { 19 | var contents = fs.readFileSync(EXTRACTS_FILE).toString('utf8'); 20 | 21 | compileStep.addJavaScript({ 22 | path: 'server/extracts.msgfmt.js', 23 | sourcePath: process.cwd() + '/' + EXTRACTS_FILE, 24 | //data: 'console.log("NODE_ENV="+process.env.NODE_ENV); if (1 || process.env.NODE_ENV === "production") msgfmt.addNative.apply(msgfmt, ' + contents + ');' 25 | data: 'if (process.env.NODE_ENV === "production") msgfmt.addNative.apply(msgfmt, ' + contents + ');' 26 | }); 27 | } 28 | } 29 | 30 | Plugin.registerSourceHandler('msgfmt', msgfmtHandler); 31 | -------------------------------------------------------------------------------- /packages/core/lib/locale-all.js: -------------------------------------------------------------------------------- 1 | MessageFormat.locale.af = function ( n ) { 2 | if ( n === 1 ) { 3 | return "one"; 4 | } 5 | return "other"; 6 | }; 7 | MessageFormat.locale.am = function(n) { 8 | if (n === 0 || n == 1) { 9 | return 'one'; 10 | } 11 | return 'other'; 12 | }; 13 | MessageFormat.locale.ar = function(n) { 14 | if (n === 0) { 15 | return 'zero'; 16 | } 17 | if (n == 1) { 18 | return 'one'; 19 | } 20 | if (n == 2) { 21 | return 'two'; 22 | } 23 | if ((n % 100) >= 3 && (n % 100) <= 10 && n == Math.floor(n)) { 24 | return 'few'; 25 | } 26 | if ((n % 100) >= 11 && (n % 100) <= 99 && n == Math.floor(n)) { 27 | return 'many'; 28 | } 29 | return 'other'; 30 | }; 31 | MessageFormat.locale.bg = function ( n ) { 32 | if ( n === 1 ) { 33 | return "one"; 34 | } 35 | return "other"; 36 | }; 37 | MessageFormat.locale.bn = function ( n ) { 38 | if ( n === 1 ) { 39 | return "one"; 40 | } 41 | return "other"; 42 | }; 43 | MessageFormat.locale.br = function (n) { 44 | if (n === 0) { 45 | return 'zero'; 46 | } 47 | if (n == 1) { 48 | return 'one'; 49 | } 50 | if (n == 2) { 51 | return 'two'; 52 | } 53 | if (n == 3) { 54 | return 'few'; 55 | } 56 | if (n == 6) { 57 | return 'many'; 58 | } 59 | return 'other'; 60 | }; 61 | MessageFormat.locale.ca = function ( n ) { 62 | if ( n === 1 ) { 63 | return "one"; 64 | } 65 | return "other"; 66 | }; 67 | MessageFormat.locale.cs = function (n) { 68 | if (n == 1) { 69 | return 'one'; 70 | } 71 | if (n == 2 || n == 3 || n == 4) { 72 | return 'few'; 73 | } 74 | return 'other'; 75 | }; 76 | MessageFormat.locale.cy = function (n) { 77 | if (n === 0) { 78 | return 'zero'; 79 | } 80 | if (n == 1) { 81 | return 'one'; 82 | } 83 | if (n == 2) { 84 | return 'two'; 85 | } 86 | if (n == 3) { 87 | return 'few'; 88 | } 89 | if (n == 6) { 90 | return 'many'; 91 | } 92 | return 'other'; 93 | }; 94 | MessageFormat.locale.da = function ( n ) { 95 | if ( n === 1 ) { 96 | return "one"; 97 | } 98 | return "other"; 99 | }; 100 | MessageFormat.locale.de = function ( n ) { 101 | if ( n === 1 ) { 102 | return "one"; 103 | } 104 | return "other"; 105 | }; 106 | MessageFormat.locale.el = function ( n ) { 107 | if ( n === 1 ) { 108 | return "one"; 109 | } 110 | return "other"; 111 | }; 112 | MessageFormat.locale.en = function ( n ) { 113 | if ( n === 1 ) { 114 | return "one"; 115 | } 116 | return "other"; 117 | }; 118 | MessageFormat.locale.es = function ( n ) { 119 | if ( n === 1 ) { 120 | return "one"; 121 | } 122 | return "other"; 123 | }; 124 | MessageFormat.locale.et = function ( n ) { 125 | if ( n === 1 ) { 126 | return "one"; 127 | } 128 | return "other"; 129 | }; 130 | MessageFormat.locale.eu = function ( n ) { 131 | if ( n === 1 ) { 132 | return "one"; 133 | } 134 | return "other"; 135 | }; 136 | MessageFormat.locale.fa = function ( n ) { 137 | return "other"; 138 | }; 139 | MessageFormat.locale.fi = function ( n ) { 140 | if ( n === 1 ) { 141 | return "one"; 142 | } 143 | return "other"; 144 | }; 145 | MessageFormat.locale.fil = function(n) { 146 | if (n === 0 || n == 1) { 147 | return 'one'; 148 | } 149 | return 'other'; 150 | }; 151 | MessageFormat.locale.fr = function (n) { 152 | if (n >= 0 && n < 2) { 153 | return 'one'; 154 | } 155 | return 'other'; 156 | }; 157 | MessageFormat.locale.ga = function (n) { 158 | if (n == 1) { 159 | return 'one'; 160 | } 161 | if (n == 2) { 162 | return 'two'; 163 | } 164 | return 'other'; 165 | }; 166 | MessageFormat.locale.gl = function ( n ) { 167 | if ( n === 1 ) { 168 | return "one"; 169 | } 170 | return "other"; 171 | }; 172 | MessageFormat.locale.gsw = function ( n ) { 173 | if ( n === 1 ) { 174 | return "one"; 175 | } 176 | return "other"; 177 | }; 178 | MessageFormat.locale.gu = function ( n ) { 179 | if ( n === 1 ) { 180 | return "one"; 181 | } 182 | return "other"; 183 | }; 184 | MessageFormat.locale.he = function ( n ) { 185 | if ( n === 1 ) { 186 | return "one"; 187 | } 188 | return "other"; 189 | }; 190 | MessageFormat.locale.hi = function(n) { 191 | if (n === 0 || n == 1) { 192 | return 'one'; 193 | } 194 | return 'other'; 195 | }; 196 | MessageFormat.locale.hr = function (n) { 197 | if ((n % 10) == 1 && (n % 100) != 11) { 198 | return 'one'; 199 | } 200 | if ((n % 10) >= 2 && (n % 10) <= 4 && 201 | ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { 202 | return 'few'; 203 | } 204 | if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || 205 | ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { 206 | return 'many'; 207 | } 208 | return 'other'; 209 | }; 210 | MessageFormat.locale.hu = function(n) { 211 | return 'other'; 212 | }; 213 | MessageFormat.locale.id = function(n) { 214 | return 'other'; 215 | }; 216 | MessageFormat.locale["in"] = function(n) { 217 | return 'other'; 218 | }; 219 | MessageFormat.locale.is = function ( n ) { 220 | if ( n === 1 ) { 221 | return "one"; 222 | } 223 | return "other"; 224 | }; 225 | MessageFormat.locale.it = function ( n ) { 226 | if ( n === 1 ) { 227 | return "one"; 228 | } 229 | return "other"; 230 | }; 231 | MessageFormat.locale.iw = function ( n ) { 232 | if ( n === 1 ) { 233 | return "one"; 234 | } 235 | return "other"; 236 | }; 237 | MessageFormat.locale.ja = function ( n ) { 238 | return "other"; 239 | }; 240 | MessageFormat.locale.kn = function ( n ) { 241 | return "other"; 242 | }; 243 | MessageFormat.locale.ko = function ( n ) { 244 | return "other"; 245 | }; 246 | MessageFormat.locale.lag = function (n) { 247 | if (n === 0) { 248 | return 'zero'; 249 | } 250 | if (n > 0 && n < 2) { 251 | return 'one'; 252 | } 253 | return 'other'; 254 | }; 255 | MessageFormat.locale.ln = function(n) { 256 | if (n === 0 || n == 1) { 257 | return 'one'; 258 | } 259 | return 'other'; 260 | }; 261 | MessageFormat.locale.lt = function (n) { 262 | if ((n % 10) == 1 && ((n % 100) < 11 || (n % 100) > 19)) { 263 | return 'one'; 264 | } 265 | if ((n % 10) >= 2 && (n % 10) <= 9 && 266 | ((n % 100) < 11 || (n % 100) > 19) && n == Math.floor(n)) { 267 | return 'few'; 268 | } 269 | return 'other'; 270 | }; 271 | MessageFormat.locale.lv = function (n) { 272 | if (n === 0) { 273 | return 'zero'; 274 | } 275 | if ((n % 10) == 1 && (n % 100) != 11) { 276 | return 'one'; 277 | } 278 | return 'other'; 279 | }; 280 | MessageFormat.locale.mk = function (n) { 281 | if ((n % 10) == 1 && n != 11) { 282 | return 'one'; 283 | } 284 | return 'other'; 285 | }; 286 | MessageFormat.locale.ml = function ( n ) { 287 | if ( n === 1 ) { 288 | return "one"; 289 | } 290 | return "other"; 291 | }; 292 | MessageFormat.locale.mo = function (n) { 293 | if (n == 1) { 294 | return 'one'; 295 | } 296 | if (n === 0 || n != 1 && (n % 100) >= 1 && 297 | (n % 100) <= 19 && n == Math.floor(n)) { 298 | return 'few'; 299 | } 300 | return 'other'; 301 | }; 302 | MessageFormat.locale.mr = function ( n ) { 303 | if ( n === 1 ) { 304 | return "one"; 305 | } 306 | return "other"; 307 | }; 308 | MessageFormat.locale.ms = function ( n ) { 309 | return "other"; 310 | }; 311 | MessageFormat.locale.mt = function (n) { 312 | if (n == 1) { 313 | return 'one'; 314 | } 315 | if (n === 0 || ((n % 100) >= 2 && (n % 100) <= 4 && n == Math.floor(n))) { 316 | return 'few'; 317 | } 318 | if ((n % 100) >= 11 && (n % 100) <= 19 && n == Math.floor(n)) { 319 | return 'many'; 320 | } 321 | return 'other'; 322 | }; 323 | MessageFormat.locale.nl = function ( n ) { 324 | if ( n === 1 ) { 325 | return "one"; 326 | } 327 | return "other"; 328 | }; 329 | MessageFormat.locale.no = function ( n ) { 330 | if ( n === 1 ) { 331 | return "one"; 332 | } 333 | return "other"; 334 | }; 335 | MessageFormat.locale.or = function ( n ) { 336 | if ( n === 1 ) { 337 | return "one"; 338 | } 339 | return "other"; 340 | }; 341 | MessageFormat.locale.pl = function (n) { 342 | if (n == 1) { 343 | return 'one'; 344 | } 345 | if ((n % 10) >= 2 && (n % 10) <= 4 && 346 | ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { 347 | return 'few'; 348 | } 349 | if ((n % 10) === 0 || n != 1 && (n % 10) == 1 || 350 | ((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) && 351 | n == Math.floor(n)) { 352 | return 'many'; 353 | } 354 | return 'other'; 355 | }; 356 | MessageFormat.locale.pt = function ( n ) { 357 | if ( n === 1 ) { 358 | return "one"; 359 | } 360 | return "other"; 361 | }; 362 | MessageFormat.locale.ro = function (n) { 363 | if (n == 1) { 364 | return 'one'; 365 | } 366 | if (n === 0 || n != 1 && (n % 100) >= 1 && 367 | (n % 100) <= 19 && n == Math.floor(n)) { 368 | return 'few'; 369 | } 370 | return 'other'; 371 | }; 372 | MessageFormat.locale.ru = function (n) { 373 | if ((n % 10) == 1 && (n % 100) != 11) { 374 | return 'one'; 375 | } 376 | if ((n % 10) >= 2 && (n % 10) <= 4 && 377 | ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { 378 | return 'few'; 379 | } 380 | if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || 381 | ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { 382 | return 'many'; 383 | } 384 | return 'other'; 385 | }; 386 | MessageFormat.locale.shi = function(n) { 387 | if (n >= 0 && n <= 1) { 388 | return 'one'; 389 | } 390 | if (n >= 2 && n <= 10 && n == Math.floor(n)) { 391 | return 'few'; 392 | } 393 | return 'other'; 394 | }; 395 | MessageFormat.locale.sk = function (n) { 396 | if (n == 1) { 397 | return 'one'; 398 | } 399 | if (n == 2 || n == 3 || n == 4) { 400 | return 'few'; 401 | } 402 | return 'other'; 403 | }; 404 | MessageFormat.locale.sl = function (n) { 405 | if ((n % 100) == 1) { 406 | return 'one'; 407 | } 408 | if ((n % 100) == 2) { 409 | return 'two'; 410 | } 411 | if ((n % 100) == 3 || (n % 100) == 4) { 412 | return 'few'; 413 | } 414 | return 'other'; 415 | }; 416 | MessageFormat.locale.sq = function ( n ) { 417 | if ( n === 1 ) { 418 | return "one"; 419 | } 420 | return "other"; 421 | }; 422 | MessageFormat.locale.sr = function (n) { 423 | if ((n % 10) == 1 && (n % 100) != 11) { 424 | return 'one'; 425 | } 426 | if ((n % 10) >= 2 && (n % 10) <= 4 && 427 | ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { 428 | return 'few'; 429 | } 430 | if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || 431 | ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { 432 | return 'many'; 433 | } 434 | return 'other'; 435 | }; 436 | MessageFormat.locale.sv = function ( n ) { 437 | if ( n === 1 ) { 438 | return "one"; 439 | } 440 | return "other"; 441 | }; 442 | MessageFormat.locale.sw = function ( n ) { 443 | if ( n === 1 ) { 444 | return "one"; 445 | } 446 | return "other"; 447 | }; 448 | MessageFormat.locale.ta = function ( n ) { 449 | if ( n === 1 ) { 450 | return "one"; 451 | } 452 | return "other"; 453 | }; 454 | MessageFormat.locale.te = function ( n ) { 455 | if ( n === 1 ) { 456 | return "one"; 457 | } 458 | return "other"; 459 | }; 460 | MessageFormat.locale.th = function ( n ) { 461 | return "other"; 462 | }; 463 | MessageFormat.locale.tl = function(n) { 464 | if (n === 0 || n == 1) { 465 | return 'one'; 466 | } 467 | return 'other'; 468 | }; 469 | MessageFormat.locale.tr = function(n) { 470 | return 'other'; 471 | }; 472 | MessageFormat.locale.uk = function (n) { 473 | if ((n % 10) == 1 && (n % 100) != 11) { 474 | return 'one'; 475 | } 476 | if ((n % 10) >= 2 && (n % 10) <= 4 && 477 | ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { 478 | return 'few'; 479 | } 480 | if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || 481 | ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { 482 | return 'many'; 483 | } 484 | return 'other'; 485 | }; 486 | MessageFormat.locale.ur = function ( n ) { 487 | if ( n === 1 ) { 488 | return "one"; 489 | } 490 | return "other"; 491 | }; 492 | MessageFormat.locale.vi = function ( n ) { 493 | return "other"; 494 | }; 495 | MessageFormat.locale.zh = function ( n ) { 496 | return "other"; 497 | }; 498 | -------------------------------------------------------------------------------- /packages/core/lib/msgfmt-client-integrations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Let's try keep msgfmt "magical", so it will detect and work with 3 | * other packages automatically. 4 | */ 5 | 6 | // GLOBAL integrations (works with any package that supplies these global vars) 7 | 8 | var integrations = { 9 | 'moment': 'locale', 10 | 'ParsleyValidator': 'setLocale' 11 | }; 12 | 13 | msgfmt.on('localeChange', function(locale) { 14 | for (var name in integrations) 15 | if (window[name] && window[name][integrations[name]]) 16 | window[name][integrations[name]](locale); 17 | }); 18 | 19 | // ---------------- package intrgrations (weak deps) ------------------------ // 20 | 21 | -------------------------------------------------------------------------------- /packages/core/lib/msgfmt-client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Settings that are required during load and should be injected into initialHtml 3 | */ 4 | mfPkg.native = 'en'; 5 | mfPkg.useLocalStorage = true; 6 | // mfPkg.sendPolicy = 'current'; 7 | mfPkg.sendPolicy = 'all'; 8 | 9 | /* 10 | * Other settings which can be passed on client startup 11 | */ 12 | mfPkg.waitOnLoaded = false; 13 | 14 | /* 15 | * Reactive vars and reactive shortcut getters that the project uses 16 | */ 17 | function getOrEquals(partial, equals) { 18 | if (equals) { 19 | if (partial.equals) 20 | return partial.equals(equals); // hopefully implemented soon 21 | else 22 | return partial.get() === equals; // runs every time 23 | } else 24 | return partial.get(); 25 | } 26 | 27 | function eachHelper(func) { 28 | _.each(['locale', 'lang', 'dir', 'loading'], function(what) { 29 | var key = '_' + what; 30 | func(key, what); 31 | }); 32 | } 33 | 34 | eachHelper(function(key, what) { 35 | msgfmt[key] = new ReactiveVar(); 36 | msgfmt[what] = _.partial(getOrEquals, msgfmt[key]); 37 | }); 38 | 39 | // Blaze support is now optional 40 | if (Package.templating) { 41 | var Template = Package.templating.Template; 42 | var Blaze = Package.blaze.Blaze; // implied by `templating` 43 | var HTML = Package.htmljs.HTML; // implied by `blaze` 44 | var Spacebars = Package.spacebars.Spacebars; 45 | 46 | // Template helpers for our reactive functions (see above) 47 | eachHelper(function(key, what) { 48 | var helperName = 'msgfmt' + what.charAt(0).toUpperCase() + what.substr(1); 49 | Template.registerHelper(helperName, msgfmt[what]); 50 | }); 51 | 52 | /* 53 | * Main Blaze regular helper / block helper, calls mf() with correct 54 | * parameters. On the client, mf() honors the Session locale if none is 55 | * manually specified here (see messageformat.js), making this a reactive 56 | * data source. 57 | */ 58 | Blaze.Template.registerHelper("mf", function(key, message, params) { 59 | // For best performance, waiton mfPkg.ready() before drawing template 60 | var dep = mfPkg.updated(); 61 | 62 | if (arguments[arguments.length-1] instanceof Spacebars.kw) { 63 | 64 | var result; 65 | var _HTML = params && (params._HTML || params._html); 66 | 67 | message = params ? message : null; 68 | params = params ? params.hash : {}; 69 | 70 | result = mf(key, params, message, params ? params.LOCALE : null); 71 | return _HTML ? 72 | Spacebars.SafeString(msgfmt.sanitizeHTML(result, _HTML)) : result; 73 | 74 | } else { 75 | 76 | // Block helpers expects a template to be returned 77 | return mfTpl; 78 | 79 | } 80 | 81 | }); 82 | 83 | var mfTpl = new Template('mf', function() { 84 | var view = this; 85 | 86 | var templateInstance = view.templateInstance(); 87 | var params = templateInstance.data; 88 | 89 | var result, message = ''; 90 | var _HTML = params._HTML || params._html; 91 | 92 | if (view.templateContentBlock) { 93 | message = Blaze._toText(view.templateContentBlock, HTML.TEXTMODE.STRING); 94 | } 95 | 96 | result = mf(params.KEY, params, message, params ? params.LOCALE : null); 97 | return _HTML ? HTML.Raw(msgfmt.sanitizeHTML(result, _HTML)) : result; 98 | }); 99 | 100 | } /* if (Package.templating) */ 101 | 102 | mfPkg.strings = mfPkg.useLocalStorage ? amplify.store('mfStrings') || {} : {}; 103 | mfPkg.mfStringsSub = Meteor.subscribe('mfStrings', 'notReady'); 104 | 105 | mfPkg.clientInit = function(native, options) { 106 | var key; 107 | 108 | if (!options) 109 | options = {}; 110 | 111 | if (options.sendPolicy) 112 | this.sendPolicy = options.sendPolicy; 113 | 114 | if (!mfPkg.strings[native]) 115 | mfPkg.strings[native] = {}; 116 | 117 | // Note, even if this is the case, we might have "preloaded" last used lang 118 | //if (mfPkg.sendPolicy === 'all') 119 | // mfPkg.loadLangs('all', updateSubs); 120 | 121 | log.debug('clientInit, ' + (Date.now() - times.loading) + 122 | 'ms after script loading'); 123 | } 124 | 125 | /* 126 | * Fetch lang data from server, more efficiently than through a 127 | * collection publish (which we only use when editing translations) 128 | */ 129 | 130 | mfPkg.loadLangs = function(reqLang) { 131 | // TODO sendpolicy:all? 132 | 133 | if (mfPkg.waitOnLoaded) { 134 | log.debug('loading() set to "' + reqLang + '"'); 135 | msgfmt._loading.set(reqLang); 136 | } 137 | 138 | var start = Date.now(); 139 | log.debug("mfLoadLangs(" + reqLang + ") sending request..."); 140 | 141 | Meteor.call('mfLoadLangs', reqLang, function(error, data) { 142 | log.debug("mfLoadLangs(" + reqLang + ") returned after " + 143 | (Date.now() - start) + 'ms'); 144 | 145 | if (error) 146 | throw new Error(error); 147 | 148 | for (lang in data.strings) { 149 | mfPkg.strings[lang] = data.strings[lang]; 150 | mfPkg.compiled[lang] = {}; // reset if exists 151 | } 152 | 153 | mfPkg.lastSync[reqLang || 'all'] = data.lastSync; 154 | localeReady(); 155 | }); 156 | }; 157 | 158 | /* 159 | * Reactive ready function. All our subscriptions are dependencies. 160 | * Additionally, this is set to false when loadLang is called, and 161 | * true when it returns. 162 | */ 163 | /* 164 | mfPkg.readyDep = new Deps.Dependency; 165 | mfPkg.ready = function() { 166 | var ready = !mfPkg.langsLoading && mfPkg.mfStringsSub.ready(); 167 | //console.log('changed to: ' + ready); 168 | this.readyDep.depend(); 169 | return ready; 170 | } 171 | */ 172 | 173 | /* 174 | * Similar to the above, but only gets invalidated each time ready() set to true 175 | */ 176 | mfPkg.updatedDep = new Deps.Dependency; 177 | mfPkg.updatedCurrent = false; 178 | mfPkg.updated = function() { 179 | this.updatedDep.depend(); 180 | return null; 181 | } 182 | 183 | /* 184 | Tracker.autorun(function() { 185 | if (mfPkg.ready() && !mfPkg.updatedCurrent) { 186 | mfPkg.updatedCurrent = true; 187 | mfPkg.updatedDep.changed(); 188 | } else if (mfPkg.updatedCurrent) { 189 | mfPkg.updatedCurrent = false; 190 | } 191 | }); 192 | */ 193 | 194 | var times = { fetches: {} }; 195 | times.loading = Date.now(); 196 | 197 | Meteor.startup(function() { 198 | times.meteorStartup = Date.now(); 199 | log.debug('Meteor.startup(), ' + 200 | (times.meteorStartup - times.loading) + 'ms after script loading'); 201 | }); 202 | 203 | mfPkg._initialFetches = []; 204 | function fetchLocale(locale) { 205 | var url, unique; 206 | 207 | if (mfPkg.lastSync[locale] && 208 | mfPkg.lastSync[locale] === mfPkg.timestamps[locale]) { 209 | log.debug('fetchLocale request for "' + locale + '", ' + 210 | 'have latest already, aborting'); 211 | return; 212 | } 213 | 214 | if (mfPkg.waitOnLoaded) { 215 | log.debug('loading() set to "' + locale + '"'); 216 | msgfmt._loading.set(locale); 217 | } 218 | 219 | if (mfPkg.sendPolicy === 'all') 220 | locale = 'all'; 221 | 222 | unique = locale + '/' + (mfPkg.lastSync[locale] || 0); 223 | url = Meteor.absoluteUrl('msgfmt/locale/' + unique, 224 | { secure: window.location.protocol === "https:" }); 225 | log.debug('fetchLocale request for "' + locale + '", url: ' + url); 226 | times.fetches[unique] = Date.now(); 227 | 228 | // TODO, settimeout make sure it arrives 229 | 230 | var s = document.createElement('script'); 231 | s.setAttribute('type', 'text/javascript'); 232 | s.setAttribute('src', url); 233 | // First load (only) should be block layout 234 | if (mfPkg._initialFetches.length > 0) 235 | s.setAttribute('async', 'async'); 236 | document.head.appendChild(s); 237 | //document.head.insertBefore(s, document.head.children[0]); 238 | }; 239 | 240 | /* 241 | * Only change reactive vars when the locale is ready (depending on user 242 | * settings) 243 | */ 244 | function localeReady(locale, dontStore) { 245 | if (!locale) { 246 | locale = mfPkg._loadingLocale; 247 | mfPkg._loadingLocale = false; 248 | } else if (dontStore) { 249 | mfPkg._loadingLocale = false; 250 | } 251 | 252 | // Used for Session.set('locale') backcompat. 253 | mfPkg.sessionLocale = locale; 254 | 255 | // Always the lang component (without dialect or encoding) 256 | lang = locale.substr(0, 2); msgfmt._lang.set(lang); 257 | dir = msgfmt.dirFromLang(lang); msgfmt._dir.set(dir); 258 | if ($body && msgfmt.setBodyDir) $body.attr('dir', dir); 259 | 260 | /* 261 | if (mfPkg.sendPolicy !== 'all') { 262 | // If we requested the lang previously, or requesting native lang, 263 | // don't retrieve the strings [again], just update the subscription 264 | if (mfPkg.strings[locale] || (!mfPkg.sendNative && locale == mfPkg.native)) 265 | updateSubs(); 266 | else { 267 | //mfPkg.loadLangs(locale, updateSubs); 268 | $.getScript('/') 269 | } 270 | } 271 | */ 272 | 273 | // backcompat 274 | Session.set('locale', locale); 275 | 276 | msgfmt._locale.set(locale); 277 | log.debug('locale set to ' + locale); 278 | 279 | if (mfPkg.waitOnLoaded) { 280 | msgfmt._loading.set(false); 281 | log.debug('loading() set to false'); 282 | } 283 | 284 | msgfmt._Event.emit('localeChange', locale); 285 | 286 | // Lang data was changed 287 | if (!dontStore) { 288 | mfPkg.updatedDep.changed(); 289 | 290 | // We can do this after layout completes 291 | if (mfPkg.useLocalStorage) 292 | _.defer(function() { 293 | amplify.store('mfLastSync', mfPkg.lastSync); 294 | amplify.store('mfStrings', mfPkg.strings); 295 | }); 296 | } 297 | } 298 | 299 | /* 300 | * ~~Simple placeholder for now~~. Future improvements detailed in 301 | * https://github.com/gadicc/meteor-messageformat/issues/38 302 | */ 303 | mfPkg.setLocale = function(locale, dontStore) { 304 | var lang, dir; 305 | 306 | if (typeof locale !== 'string') 307 | return log.debug('Ignoring setLocale(' + locale + '), expecting a string...'); 308 | 309 | // If "en_US" doesn't exist, fallback to "en" and then native 310 | // Not really that useful since user usually picks lang from a list 311 | // and headerLocale() does this anyway with it's own logic. 312 | if (!mfPkg.timestamps[locale]) { 313 | locale = locale.split('_')[0]; 314 | if (!mfPkg.timestamps[locale]) 315 | locale = mfPkg.native; 316 | } 317 | 318 | // This locale changing is already pending 319 | if (msgfmt._loadingLocale === locale) 320 | return log.debug('setLocale (already loading)', locale, dontStore); 321 | 322 | // If there's no actual change, stop here (nonreactive get) 323 | if (locale === msgfmt._locale.curValue) 324 | return log.trace('setLocale (dupe)', locale, dontStore); 325 | else 326 | log.debug('setLocale', locale, dontStore); 327 | 328 | msgfmt._loadingLocale = locale; 329 | 330 | /* 331 | * So that server-side mf() calls know which locale to use 332 | * https://github.com/gadicc/meteor-messageformat/issues/83 333 | */ 334 | Meteor.call('msgfmt:setLocale', locale); 335 | 336 | // At the end of this file, we'll restore this value on load if it exists 337 | if (!dontStore && mfPkg.useLocalStorage) 338 | amplify.store('mfLocale', locale); 339 | 340 | if (!mfPkg.waitOnLoaded) 341 | localeReady(locale); 342 | 343 | // actually we want: 344 | // 1. init load via http, 2. other loads via method, 3. updates via subs 345 | // don't forget, offline, appcache, disallow inline, etc 346 | 347 | //if (mfPkg.sendPolicy !== 'all') 348 | if (mfPkg._initialFetches.length === 0) { 349 | // First load is always via HTTP, 350 | fetchLocale(locale); 351 | } else if (!_.contains(mfPkg._initialFetches, locale) 352 | && mfPkg.sendPolicy !== 'all') { 353 | // leverage existing connection 354 | mfPkg.loadLangs(locale); 355 | } else { 356 | // TODO, subs etc 357 | 358 | // Nothing to load, just set locale. Mark as ready if we didn't before. 359 | if (mfPkg.waitOnLoaded) 360 | localeReady(locale, true /* dontStore */); 361 | } 362 | 363 | return locale; 364 | } 365 | 366 | /* 367 | * Update our subscription for language updates. If we change languages, we'll 368 | * we'll still have all the lang data in mfPkg, we just stop getting updates for 369 | * that language. If we change back, we'll get all the updates since our last 370 | * sync for that lang. 371 | */ 372 | function updateSubs() { 373 | var locale = msgfmt.locale() || mfPkg.native; 374 | mfPkg.observeFrom(mfPkg.lastSync[locale]); 375 | if (mfPkg.mfStringsSub) 376 | mfPkg.mfStringsSub.stop(); 377 | mfPkg.mfStringsSub 378 | = Meteor.subscribe('mfStrings', locale, 379 | mfPkg.lastSync[locale], false); 380 | } 381 | 382 | /* 383 | * fetchLocale initiates a request to the server, which returns to here 384 | */ 385 | msgfmt.fromServer = function(data) { 386 | var target, request = data._request, locale = request.split('/')[0]; 387 | log.debug('fetchLocale arrived ' + (Date.now() - 388 | times.fetches[data._request]) + 'ms after request'); 389 | delete times.fetches[data._request]; 390 | delete data._request; 391 | 392 | if (!mfPkg.lastSync.all) 393 | mfPkg.lastSync.all = 0; 394 | 395 | if (locale === 'all') 396 | _.each(_.keys(data), function(locale) { 397 | mfPkg.lastSync[locale] = data[locale]._updatedAt; 398 | if (data[locale]._updatedAt > mfPkg.lastSync.all) 399 | mfPkg.lastSync.all = data[locale]._updatedAt; 400 | delete data[locale]._updatedAt; 401 | }); 402 | else { 403 | mfPkg.lastSync[locale] = data._updatedAt; 404 | delete data._updatedAt; 405 | } 406 | 407 | target = msgfmt.strings; 408 | if (locale !== 'all') { 409 | if (!target[locale]) 410 | target[locale] = {}; 411 | target = target[locale]; 412 | } 413 | 414 | jQuery.extend(true /* deep */, target, data); 415 | 416 | if (locale === 'all') { 417 | locale = msgfmt._loadingLocale; 418 | if (!_.contains(mfPkg._initialFetches, 'all')) 419 | mfPkg._initialFetches.push('all'); 420 | } else { 421 | if (!_.contains(mfPkg._initialFetches, locale)) 422 | mfPkg._initialFetches.push(locale); 423 | } 424 | 425 | localeReady(locale); 426 | }; 427 | 428 | mfPkg.resetStorage = function() { 429 | _.each(['mfLastSync', 'mfLocale', 'mfStrings'], function(what) { 430 | amplify.store(what, null); 431 | }); 432 | window.location += ''; 433 | } 434 | 435 | /* code below involves loading the actual module at long time */ 436 | 437 | var injected = Injected.obj('msgfmt'); 438 | if (injected) { 439 | mfPkg.native = injected.native; 440 | mfPkg.sendPolicy = injected.sendPolicy; 441 | mfPkg.timestamps = injected.locales; 442 | } else { 443 | log.debug('Injected object was undefined, this is most likely a Cordova session'); 444 | mfPkg.timestamps = {}; 445 | var time = (new Date()).getTime(); 446 | if (Meteor.settings && Meteor.settings.public && Meteor.settings.public.msgfmt) { 447 | var msgfmtSettings = Meteor.settings.public.msgfmt; 448 | mfPkg.native = msgfmtSettings.native; 449 | _.each(msgfmtSettings.locales, function(locale) { 450 | mfPkg.timestamps[locale] = time; 451 | }); 452 | } else { 453 | log.warn('Cordova builds have issues with the inject-initial package, make sure to define settings keys public.localization.native && public.localization.locales'); 454 | mfPkg.native = injected.native; 455 | mfPkg.timestamps[mfPkg.native] = time; 456 | } 457 | } 458 | 459 | if (mfPkg.timestamps) { 460 | (function() { 461 | var key, max = 0; 462 | mfPkg.locales = []; 463 | for (key in mfPkg.timestamps) { 464 | if (mfPkg.timestamps[key] > max) 465 | max = mfPkg.timestamps[key]; 466 | mfPkg.locales.push(key); 467 | } 468 | mfPkg.timestamps.all = max; 469 | })(); 470 | } 471 | 472 | // don't need this anymore? 473 | //if (!msgfmt.strings[msgfmt.native]) 474 | // msgfmt.strings[msgfmt.native] = {}; 475 | 476 | mfPkg.lastSync = mfPkg.useLocalStorage ? amplify.store('mfLastSync') || {} : {}; 477 | 478 | var $body; 479 | if (msgfmt.setBodyDir) { 480 | $(function() { 481 | $body = $(document.body).attr('dir', msgfmt.dir()); 482 | }); 483 | } 484 | 485 | /* 486 | * When mfPkg.setLocale(locale) is called, we store that value with amplify. 487 | * On load, we can re-set the locale to the last user supplied value. 488 | * Note, the use of Session here is intentional, to survive hot code pushes 489 | */ 490 | var locale = mfPkg.useLocalStorage && amplify.store('mfLocale'); 491 | if (locale) { 492 | log.debug('Found stored locale "' + locale + '"'); 493 | mfPkg.setLocale(locale, true /* dontStore */); 494 | } else if (locale = Session.get('locale')) { 495 | log.debug('Found session locale "' + locale + '"'); 496 | mfPkg.setLocale(locale); 497 | } else if (injected && injected.headerLocale) { 498 | locale = injected.headerLocale 499 | log.debug('Setting locale from header: ' + locale); 500 | mfPkg.setLocale(locale); 501 | } else { 502 | mfPkg.setLocale(msgfmt.native); 503 | } 504 | 505 | // backcompat with v0, auto call setLocale() on Session.set('locale') 506 | Tracker.autorun(function() { 507 | var sessionLocale = Session.get('locale'); 508 | if (sessionLocale !== mfPkg.sessionLocale) 509 | mfPkg.setLocale(sessionLocale); 510 | }); 511 | 512 | // backcompat with v0 513 | msgfmt.ready = function(func) { 514 | if (func) { 515 | Meteor.startup(function() { 516 | log.warn("Loading startup code in mfPkg.ready() is no longer required, just use Meteor.startup()."); 517 | func(); 518 | }); 519 | } else { 520 | log.warn("Waiting on mfPkg or mfPkg.ready() is no longer required and should be removed."); 521 | return true; 522 | } 523 | }; 524 | 525 | if (msgfmt.storeUserLocale && Meteor.user) { 526 | Meteor.subscribe('msgfmt:locale'); 527 | Meteor.startup(function() { 528 | Tracker.autorun(function() { 529 | var user = Meteor.user(); 530 | if (user && user.locale && user.locale !== msgfmt._locale.curValue) { 531 | log.debug('Got new locale "' + user.locale + '" from Meteor.user()'); 532 | msgfmt.setLocale(user.locale); 533 | } 534 | }); 535 | }); 536 | } 537 | -------------------------------------------------------------------------------- /packages/core/lib/msgfmt-server-integrations.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Let's try keep msgfmt "magical", so it will detect and work with 3 | * other packages automatically. 4 | */ 5 | 6 | // GLOBAL integrations (works with any package that supplies these global vars) 7 | 8 | /* none */ 9 | 10 | // ---------------- package intrgrations (weak deps) ------------------------ // 11 | 12 | // HANDLEBARS 13 | 14 | var Handlebars = Package['cmather:handlebars-server'] && 15 | Package['cmather:handlebars-server'].Handlebars; 16 | 17 | if (Handlebars) { 18 | var OriginalHandlebars = Package['cmather:handlebars-server'].OriginalHandlebars; 19 | log.debug('Integrating with cmather:handlebars-server...'); 20 | OriginalHandlebars.registerHelper('mf', function(key, message, options) { 21 | var params = options.hash; 22 | return mf(key, params, message, params.LOCALE); 23 | }); 24 | } else { 25 | log.debug('Not integrating with cmather:handlebars-server (not found)...'); 26 | } 27 | -------------------------------------------------------------------------------- /packages/core/lib/msgfmt-server.js: -------------------------------------------------------------------------------- 1 | var Fiber = Npm.require('fibers'); 2 | 3 | // server only, but used on the client in msgfmt:ui 4 | msgfmt.mfRevisions = new Mongo.Collection('mfRevisions'); 5 | 6 | /* 7 | * New entry point for startup (native) and any language modification. Check that all 8 | * the fields we need exist. Don't test for these anywhere else. 9 | */ 10 | function checkLocaleMetaExists(locale) { 11 | _.each(['strings', 'compiled', 'meta'], function(key) { 12 | if (!mfPkg[key][locale]) 13 | mfPkg[key][locale] = {}; 14 | }); 15 | } 16 | 17 | // Load each string and update the database if necessary 18 | msgfmt.langUpdate = function(lang, strings, newMeta) { 19 | checkLocaleMetaExists(lang); 20 | 21 | /* 22 | * Update meta data 23 | */ 24 | 25 | var existingMeta = msgfmt.meta[lang]; 26 | var metas = [ 'updatedAt', 'extractedAt' ]; 27 | var updates = { }; 28 | 29 | _.each(metas, function(key) { 30 | // Upgrade from v1 or pre core@2.0.0-preview.19 31 | if (typeof newMeta.updatedAt === 'string') 32 | newMeta.updatedAt = new Date(newMeta.updatedAt).getTime(); 33 | 34 | if (!existingMeta[key] || existingMeta[key] < newMeta[key]) { 35 | updates[key] = newMeta[key]; 36 | existingMeta[key] = newMeta[key]; 37 | } 38 | }); 39 | 40 | if (!updates.updatedAt) { 41 | log.debug('Aborting unnecessary langUpdate for "' + lang + '" (' + 42 | (existingMeta.updatedAt - newMeta.updatedAt) + ' ms behind)'); 43 | return; 44 | } 45 | 46 | msgfmt.mfMeta.upsert(lang, { $set: updates }); 47 | 48 | // Aggregated "all" meta 49 | if (msgfmt.meta.all) { 50 | if (newMeta.updatedAt > msgfmt.meta.all.updatedAt) { 51 | msgfmt.meta.all.updatedAt = newMeta.updatedAt; 52 | msgfmt.mfMeta.update('all', { $set: { 53 | updatedAt: newMeta.updatedAt 54 | }}); 55 | } 56 | } else { 57 | msgfmt.meta.all = { updatedAt: newMeta.updatedAt }; 58 | msgfmt.mfMeta.insert({ _id: 'all', updatedAt: newMeta.updatedAt }); 59 | } 60 | 61 | /* 62 | * See if any of our extracted strings are newer than their copy in 63 | * the database and update them accordingly, then update mfPkg.string key 64 | */ 65 | 66 | var str, existing, revisionId, obj, updating, dbInsert, result, query, 67 | // removed in core.16; _id 68 | optional = [/*'_id',*/ 'file', 'line', 'template', 'func', 'removed', 'fuzzy']; 69 | for (key in strings) { 70 | str = strings[key]; 71 | existing = this.strings[lang][key]; 72 | 73 | // skip key if local copy is newer than this one (i.e. from other file) 74 | if (existing && existing.mtime > str.mtime) 75 | continue; 76 | 77 | // if text has changed, create a new revision, else preserve revisionId 78 | if (existing && existing.text == str.text && existing.removed == str.removed) { 79 | updating = false; 80 | revisionId = this.strings[lang][key].revisionId; 81 | } else { 82 | updating = true; 83 | revisionId = this.mfRevisions.insert({ 84 | lang: lang, 85 | key: key, 86 | text: str.text, 87 | ctime: str.ctime, 88 | }); 89 | } 90 | 91 | obj = { 92 | key: key, 93 | lang: lang, 94 | text: str.text, 95 | ctime: str.ctime, 96 | mtime: str.mtime, 97 | revisionId: revisionId 98 | }; 99 | for (var i=0; i < optional.length; i++) 100 | if (str[optional[i]]) 101 | obj[optional[i]] = str[optional[i]]; 102 | 103 | /* 104 | - since core.16 we no longer worry about _id's 105 | // insert unfound, or re-insert on wrong _id, otherwise update 106 | if (existing) { 107 | if (str._id && existing._id === str._id) { 108 | dbInsert = false; 109 | } else { 110 | // non-matching ID. remove and insert with correct ID (mfAll.js) 111 | this.mfStrings.remove({_id: existing._id}); 112 | dbInsert = true; 113 | } 114 | } else { 115 | dbInsert = true; 116 | } 117 | */ 118 | 119 | /* meteor upsert does allow _id even for insert upsert 120 | if (this.strings[lang][key] && str._id 121 | && this.strings[lang][key]._id != str._id) { 122 | console.log('remove'); 123 | this.mfStrings.remove({_id: this.strings[lang][key]._id}); 124 | } 125 | 126 | result = this.mfStrings.upsert({key: key, lang: lang}, { $set: obj }); 127 | if (result.insertedId) 128 | obj._id = insertedId; 129 | if (updating) 130 | this.strings[lang][key] = obj; 131 | */ 132 | 133 | /* 134 | if (dbInsert) { 135 | // obj._id = this.mfStrings.insert(obj) - v16 deprecates use of _id 136 | obj._id = this.mfStrings.insert(obj) 137 | } else { 138 | // this.mfStrings.update(obj._id, obj); - v16 deprecates use of _id 139 | this.mfStrings.update({ key: obj.key, lang: obj.lang }, obj); 140 | } 141 | */ 142 | this.mfStrings.upsert({ key: obj.key, lang: obj.lang }, obj); 143 | 144 | if (updating) { 145 | // does this update affect translations? 146 | if (existing && (lang == mfPkg.native || str.removed)) { 147 | query = { $set: {} }; 148 | if (lang == mfPkg.native) { 149 | // TODO, consider string comparison with threshold to mark as fuzzy 150 | query['$set'].fuzzy = true; 151 | } 152 | if (str.removed) 153 | query['$set'].removed = true; 154 | 155 | this.mfStrings.update( { key: key, lang: {$ne: lang} }, query, { multi: true }); 156 | } 157 | 158 | // finally, update the local cache 159 | this.strings[lang][key] = obj; 160 | } 161 | 162 | } /* for (key in strings) */ 163 | 164 | } 165 | 166 | function wrapAdd(origFunc, which, name) { 167 | return function(strings, meta) { 168 | var startTime = Date.now(); 169 | 170 | // not initted yet, we don't know what the native lang is 171 | if (!this.initted) { 172 | log.debug(name + ' called before init(), queueing...'); 173 | this[which+'Queue'] = { strings: strings, meta: meta }; 174 | return; 175 | } 176 | 177 | // In some cases pre core.19, used on upgrade to fix. 178 | if (!meta.updatedAt) { 179 | log.debug(name + ' meta.updatedAt was ' + meta.updatedAt, meta); 180 | meta.updatedAt = Date.now(); 181 | } 182 | 183 | var key = '_lastSync' + which.charAt(0).toUpperCase() + which.substr(1); 184 | var lastSync = msgfmt.meta[key]; 185 | if (!lastSync) 186 | lastSync = msgfmt.meta[key] = { mtime: 0 }; 187 | 188 | // We're already up to date, abort. 189 | if (meta.updatedAt <= lastSync.mtime) { 190 | log.debug(name + " already up to date..."); 191 | return; 192 | } 193 | 194 | log.info(name + ' updating...'); 195 | 196 | // Update observe before modifying database 197 | this.observeFrom(meta.updatedAt, which); 198 | 199 | // Prewrapped function 200 | origFunc.call(this, strings, meta); 201 | 202 | lastSync.mtime = meta.updatedAt; 203 | msgfmt.mfMeta.upsert(key, {$set: {mtime: meta.updatedAt } }); 204 | 205 | log.debug('Finished ' + name + ' in ' + (Date.now() - startTime) + ' ms'); 206 | } 207 | } 208 | 209 | mfPkg.addNative = wrapAdd(function(strings, meta) { 210 | this.langUpdate(mfPkg.native, strings, meta); 211 | }, 'native', 'addNative() (from extracts.msgfmt~)'); 212 | 213 | // called from mfAll.js 214 | mfPkg.syncAll = wrapAdd(function(strings, meta) { 215 | for (var lang in strings) 216 | if (lang !== msgfmt.native) 217 | msgfmt.langUpdate(lang, strings[lang], meta); 218 | 219 | // since core.19; use nativeStrings from mfAll.js too 220 | var nativeStrings = strings[msgfmt.native]; 221 | if (nativeStrings) { 222 | 223 | var max = _.max(nativeStrings, function(s) { return s.mtime; }).mtime; 224 | var nativeMeta = { updatedAt: max }; 225 | if (meta.extractedAt) 226 | nativeMeta.extractedAt = meta.extractedAt; 227 | 228 | this.addNative(nativeStrings, nativeMeta); 229 | 230 | } 231 | }, 'trans', 'syncAll() (from mfAll.js)'); 232 | 233 | var injectableOptions = ['waitOnLoaded', 'sendPolicy']; 234 | 235 | mfPkg.serverInit = function(native, options) { 236 | var queues = { nativeQueue: 'addNative', transQueue: 'syncAll' }; 237 | for (var queue in queues) { 238 | if (this[queue]) { 239 | this[queues[queue]](this[queue].strings, this[queue].meta); 240 | delete this[queue]; 241 | } 242 | } 243 | 244 | if (options) { 245 | msgfmt.options = options; 246 | 247 | _.each(injectableOptions, function(key) { 248 | if (options[key] !== undefined) 249 | msgfmt[key] = options[key]; 250 | }); 251 | } 252 | 253 | checkLocaleMetaExists(native); 254 | 255 | // maxMtime is set on initial database load (at end of this file) 256 | msgfmt.observeFrom(maxMtime); 257 | } 258 | 259 | Meteor.methods({ 260 | // Method to send language data to client, see note in client file. 261 | mfLoadLangs: function(reqLang) { 262 | check(reqLang, String); 263 | //console.log(reqLang); 264 | var strings = {}; 265 | for (lang in mfPkg.strings) { 266 | if (reqLang != lang && reqLang != 'all') 267 | continue; 268 | strings[lang] = {}; 269 | for (key in mfPkg.strings[lang]) 270 | strings[lang][key] = mfPkg.strings[lang][key].text.replace(/\s+/g, ' '); 271 | } 272 | return { 273 | strings: strings, 274 | lastSync: new Date().getTime() 275 | } 276 | } 277 | }); 278 | 279 | Meteor.publish('mfStrings', function(lang, after, fullInfo) { 280 | var query = {}, options = {}; 281 | check(lang, Match.OneOf(String, [String])); 282 | //console.log(after); 283 | check(after, Match.OneOf(Number, undefined, null)); 284 | check(fullInfo, Match.Optional(Boolean)); 285 | 286 | // fake sub 287 | if (lang == 'notReady') 288 | return this.ready(); 289 | 290 | // ['en', 'he'] or 'en' or 'native' or 'all' 291 | if (_.isArray(lang)) 292 | query.lang = {$in: lang}; 293 | else if (lang == 'native') 294 | query.lang = mfPkg.native; 295 | else if (lang != 'all') 296 | query.lang = lang; 297 | 298 | if (after) 299 | query.mtime = {$gt: after}; 300 | 301 | if (!fullInfo) 302 | options.fields = { key: 1, lang: 1, text: 1 }; 303 | 304 | return mfPkg.mfStrings.find(query, options); 305 | }); 306 | 307 | Meteor.publish('msgfmt:locale', function() { 308 | if (this.userId) 309 | return Meteor.users.find(this.userId, { fields: { locale: 1 } }); 310 | else 311 | return this.ready(); 312 | }); 313 | 314 | Meteor.methods({ 315 | 'mfPkg.langList': function() { 316 | return _.keys(mfPkg.strings); 317 | }, 318 | 'msgfmt:setLocale': function(locale) { 319 | check(locale, String); 320 | this.connection.locale = locale; 321 | if (this.userId && msgfmt.storeUserLocale) 322 | Meteor.users.update(this.userId, { $set : { locale: locale } }); 323 | } 324 | }); 325 | 326 | /* 327 | * Given an 'accept-language' header, return the best match from our 328 | * available locales 329 | */ 330 | var headerLocale = function(acceptLangs) { 331 | acceptLangs = acceptLangs.split(','); 332 | for (var i=0; i < acceptLangs.length; i++) { 333 | locale = acceptLangs[i].split(';')[0].trim(); 334 | if (mfPkg.strings[locale]) { 335 | return locale; 336 | } 337 | } 338 | return false; 339 | } 340 | 341 | /* 342 | * Data injected into initial HTML and served to client, includes 343 | * 344 | * + the native language (from server, to be available earlier on client) 345 | * + any other configuration settings we need early on the client 346 | * + last update time for all locales (and hence, list of of available locales) 347 | * + best locale match for accept-language header (if it exists) 348 | */ 349 | var msgfmtClientData = function(req) { 350 | var out = { 351 | native: mfPkg.native, 352 | locales: {} 353 | }; 354 | 355 | _.each(injectableOptions, function(key) { 356 | if (msgfmt[key] !== undefined) 357 | out[key] = msgfmt[key]; 358 | }); 359 | 360 | if (req.headers['accept-language']) 361 | out.headerLocale = headerLocale(req.headers['accept-language']); 362 | 363 | var locales = out.locales; 364 | for (var lang in mfPkg.meta) 365 | locales[lang] = mfPkg.meta[lang].updatedAt; 366 | 367 | return out; 368 | } 369 | 370 | WebApp.connectHandlers.use(function(req, res, next) { 371 | if (Inject.appUrl(req.url)) 372 | Inject.obj('msgfmt', msgfmtClientData(req), res); 373 | if (!msgfmt.options.disableIntlPolyfill) { 374 | // We use a function in case new langauges are added after first load 375 | Inject.rawHead('intlPoly', function() { 376 | return ''; 380 | }); 381 | } 382 | next(); 383 | }); 384 | 385 | // TODO, cache/optimize 386 | 387 | function localeStringsToDictionary(res, locale, mtime, flags) { 388 | var key, out = {}; 389 | res.setHeader("Content-Type", "application/javascript; charset=UTF-8"); 390 | res.writeHead(200); 391 | 392 | if (locale === 'all') { 393 | for (locale in mfPkg.strings) { 394 | out[locale] = {}; 395 | for (key in mfPkg.strings[locale]) 396 | if (mfPkg.strings[locale][key].mtime > mtime) 397 | out[locale][key] = mfPkg.strings[locale][key].text.replace(/\s+/g, ' '); 398 | out[locale]._updatedAt = mfPkg.meta[locale].updatedAt; 399 | } 400 | locale = 'all'; 401 | } else if (mfPkg.strings[locale]) { 402 | for (key in mfPkg.strings[locale]) 403 | if (mfPkg.strings[locale][key].mtime > mtime) 404 | out[key] = mfPkg.strings[locale][key].text.replace(/\s+/g, ' '); 405 | out._updatedAt = mfPkg.meta[locale].updatedAt; 406 | } 407 | out._request = locale + '/' + mtime; 408 | res.end('Package["msgfmt:core"].msgfmt.fromServer(' + 409 | JSON.stringify(out) + ');'); 410 | } 411 | 412 | Meteor.startup(function() { 413 | if (!msgfmt.initted) 414 | throw new Error("[msgfmt] Installed but msgfmt.init('en') etc was never called."); 415 | }); 416 | 417 | // TODO, caching, compression 418 | WebApp.connectHandlers.use(function(req, res, next) { 419 | if (req.url.substr(0, 15) === '/msgfmt/locale/') { 420 | var rest = req.url.substr(15).split('/'); 421 | var locale = rest[0]; 422 | var mtime = rest[1] || 0; 423 | var flags = rest.slice(2); 424 | localeStringsToDictionary(res, locale, mtime, flags); 425 | return; 426 | } 427 | next(); 428 | }); 429 | 430 | msgfmt.strings = {}; 431 | 432 | // Load meta data 433 | mfPkg.mfMeta.find().forEach(function(m) { 434 | // Update from before core.19 435 | if (m._id === 'syncExtracts' || m._id === 'syncTrans') { 436 | msgfmt.mfMeta.remove(m._id); 437 | return; 438 | } 439 | 440 | msgfmt.meta[m._id] = m; 441 | if (m._id !== 'all' && m._id.charAt(0) !== '_') 442 | checkLocaleMetaExists(m._id); 443 | delete m._id; 444 | }); 445 | 446 | /* 447 | * As of preview.16, we no longer rely on `_id` and properly set a unique compound 448 | * index on `key` and `lang`. In case the database has previous dups, let's clean 449 | * them up. 450 | */ 451 | var checkForDupes = msgfmt.meta.all && !msgfmt.meta.all._dupeFree; 452 | if (checkForDupes) { 453 | msgfmt.mfMeta.update('all', { $set: { _dupeFree: 1 }} ); 454 | msgfmt.meta.all._dupeFree = true; 455 | log.warn('Checking for dupes in mfStrings... (once off on upgrade from < core.15)'); 456 | } 457 | 458 | /* 459 | * During script load, immediately load all strings from database 460 | */ 461 | log.trace('Retrieving strings from database...'); 462 | var startTime = Date.now(); 463 | var allStrings = msgfmt.mfStrings.find().fetch(); 464 | var maxMtime = 0; 465 | log.trace('Finished retrieval in ' + (Date.now() - startTime) + ' ms'); 466 | 467 | if (checkForDupes) { 468 | _.each(allStrings, function(str) { 469 | checkLocaleMetaExists(str.lang); 470 | var existing = msgfmt.strings[str.lang][str.key]; 471 | if (existing) { 472 | log.warn('Duplicate "' + str.key + '" (' + str.lang + '), keeping latest.'); 473 | if (str.mtime > existing.mtime) { 474 | msgfmt.strings[str.lang][str.key] = str; // overwrite with newer 475 | msgfmt.mfStrings.remove({ key: existing.key, lang: existing.lang }); 476 | } else { 477 | // current str is older, remove it, don't overwrite in msgfmt.strings 478 | msgfmt.mfStrings.remove({ key: str.key, lang: str.lang }); 479 | } 480 | } else 481 | msgfmt.strings[str.lang][str.key] = str; 482 | if (str.mtime > maxMtime) maxMtime = str.mtime; 483 | }); 484 | } else { 485 | _.each(allStrings, function(str) { 486 | checkLocaleMetaExists(str.lang); 487 | msgfmt.strings[str.lang][str.key] = str; 488 | if (str.mtime > maxMtime) maxMtime = str.mtime; 489 | }); 490 | } 491 | delete allStrings; 492 | 493 | // ensure compound indexe on server only (collection exists in both) 494 | // move to top of file when we're pretty sure everyone is _dupeFree 495 | msgfmt.mfStrings._ensureIndex( { key:1, lang:1 }, { unique: true } ); 496 | -------------------------------------------------------------------------------- /packages/core/lib/msgfmt.js: -------------------------------------------------------------------------------- 1 | /* 2 | * TODO 3 | * 4 | * -> Revisions, show diff 5 | * -> Mark stuff as fuzzy or invalid depending on how big the change is 6 | * -> transUI, enable on load, etc... decide on mfTrans.js format 7 | * 8 | * sendNative code (force send of native strings in case not kept inline) 9 | * ready() function for loadlang, sub. XXX- 10 | * setLocale() 11 | * language loader tooltip 12 | * 13 | */ 14 | 15 | if (Meteor.isServer && Package['gadicohen:messageformat']) { 16 | throw new Error('You have conflicting versions of messageformat installed (v0 and v2). ' + 17 | 'Please thoroughly read the Upgrade Instructions in the README and remove `gadicohen:messageformat` ' + 18 | 'if you\d like to use v2.'); 19 | return; 20 | } 21 | 22 | // For stuff that runs before init, e.g. want correct log level 23 | log = {}; 24 | var queuedLogs = { debug: [], trace: [], warn: [], info: [] }; 25 | var defferedLog = function(text) { this.push(arguments); }; 26 | (function() { 27 | for (var key in queuedLogs) 28 | log[key] = _.bind(defferedLog, queuedLogs[key]); 29 | })(); 30 | 31 | 32 | mfPkg = msgfmt = { 33 | native: 'en', // Fine to use reserved words for IdentifierNames (vs Identifiers) 34 | objects: {}, 35 | compiled: {}, 36 | strings: {}, 37 | meta: {}, 38 | initted: false, 39 | 40 | sendPolicy: 'all', 41 | sendNative: false, 42 | transUI: { 43 | enabled: true 44 | }, 45 | 46 | setBodyDir: true, 47 | updateOnTouch: false, 48 | storeUserLocale: true, 49 | 50 | mfStrings: new Mongo.Collection('mfStrings'), 51 | mfMeta: new Mongo.Collection('mfMeta'), 52 | 53 | _currentObserves: { }, 54 | 55 | init: function(native, options) { 56 | this.native = native; 57 | this.initted = true; 58 | if (!options) 59 | options = {}; 60 | 61 | log = new Logger('msgfmt'); 62 | if (options.logLevel) { 63 | Logger.setLevel('msgfmt', options.logLevel); 64 | } 65 | 66 | for (key in queuedLogs) 67 | while (queuedLogs[key].length) 68 | log[key].apply(log, _.union(['[Q]'], queuedLogs[key].shift())); 69 | 70 | if (Meteor.isServer) 71 | this.serverInit(native, options); 72 | else 73 | this.clientInit(native, options); 74 | 75 | // For other packages (TODO, plug into init stream) 76 | this.initOptions = options; 77 | }, 78 | 79 | rtlLangs: [ 'ar', 'dv', 'fa', 'ha', 'he', 'iw', 'ji', 'ps', 'ur', 'yi' ], 80 | dirFromLang: function(lang) { 81 | return msgfmt.rtlLangs.indexOf(lang) === -1 ? 'ltr' : 'rtl'; 82 | }, 83 | 84 | /* 85 | * We want our local cache to always be up to date. By default, 86 | * we watch for all changes. But if we're going to make a batch 87 | * update to the database, no point in having these cmoe back 88 | * from the database too, so we can update the query by mtime. 89 | * 90 | * Since updates could come from multiple sources, we'll ignore 91 | * requests to observe from an earlier mtime than what we're 92 | * already using. 93 | */ 94 | observeFrom: function(mtime, which) { 95 | 96 | if (!which) { 97 | this.observeFrom(mtime, 'native'); 98 | this.observeFrom(mtime, 'trans'); 99 | return; 100 | } 101 | 102 | var observes = msgfmt._currentObserves; 103 | if (observes[which]) { 104 | if (mtime < observes[which].mtime) 105 | return; 106 | else 107 | observes[which].handle.stop(); 108 | } 109 | 110 | var query = {mtime: {$gt: mtime}}; 111 | if (which === 'native') 112 | query.lang = msgfmt.native; 113 | else if (which === 'trans') 114 | query.lang = { $ne: msgfmt.native }; 115 | 116 | var handle = this.mfStrings.find(query).observe({ 117 | added: function(doc) { 118 | // console.log('added ' + doc.key + ' ' + doc.text); 119 | if (!mfPkg.strings[doc.lang]) 120 | mfPkg.strings[doc.lang] = {}; 121 | if (!mfPkg.compiled[doc.lang]) 122 | mfPkg.compiled[doc.lang] = {}; 123 | mfPkg.strings[doc.lang][doc.key] 124 | = Meteor.isClient ? doc.text : doc; 125 | }, changed: function(doc) { 126 | // console.log('changed ' + doc.key + ' ' + doc.text); 127 | mfPkg.strings[doc.lang][doc.key] 128 | = Meteor.isClient ? doc.text : doc; 129 | if (mfPkg.compiled[doc.lang][doc.key]) 130 | delete mfPkg.compiled[doc.lang][doc.key]; 131 | } 132 | }); 133 | 134 | observes[which] = { mtime: mtime, handle: handle }; 135 | } 136 | }; 137 | 138 | mfPkg.mfMeta.deny(function() { return true; }); 139 | 140 | var formats = msgfmt.formats = { 141 | number: { 142 | USD: { 143 | style : 'currency', 144 | currency: 'USD' 145 | } 146 | } 147 | }; 148 | 149 | msgfmt.addFormat = function(which, data) { 150 | _.extend(formats[which], data); 151 | }; 152 | msgfmt.addCurrencyShortcut = function(currency) { 153 | var obj = {}; 154 | obj[currency] = { style: 'currency', currency: currency }; 155 | msgfmt.addFormat('number', obj); 156 | }; 157 | 158 | msgfmt.addFormat('number', { ZAR: { style: 'currency', currency: 'ZAR' } }); 159 | msgfmt.addCurrencyShortcut('ILS'); 160 | 161 | mf = function mfcall(key, params, message, locale) { 162 | var tmp; 163 | 164 | if (!locale) { 165 | if (Meteor.isClient) { 166 | locale = Session.get('locale'); 167 | } else { 168 | var currentInvocation = DDP._CurrentInvocation.get(); 169 | if (currentInvocation) 170 | locale = currentInvocation.connection.locale; 171 | else { 172 | log.warn( 173 | "You called mf() with the key '" + key + 174 | "' outside of a method/publish and " + 175 | "without specifying a locale, defaulting to native (" + msgfmt.native + ")" 176 | ); 177 | locale = msgfmt.native; 178 | } 179 | 180 | } 181 | } 182 | if (!locale || (!mfPkg.strings[locale] && !mfPkg.compiled[locale])) 183 | locale = mfPkg.native; 184 | if (_.isString(params)) { 185 | tmp = message; 186 | message = params; 187 | params = tmp; 188 | } 189 | 190 | var mf = mfPkg.objects[locale]; 191 | if (!mf) { 192 | mf = mfPkg.objects[locale] = 1; //new MessageFormat(locale); 193 | if (!mfPkg.strings[locale]) mfPkg.strings[locale] = {}; 194 | if (!mfPkg.compiled[locale]) mfPkg.compiled[locale] = {}; 195 | } 196 | 197 | var cmessage = mfPkg.compiled[locale][key]; 198 | if (!cmessage) { 199 | // Client can't do inline eval and compiled hasn't arrived yet 200 | if (mfPkg.sendCompiled) 201 | return message; 202 | 203 | // try find key in 1) locale, 2) native, 3) as an argument, 4) just show the key name 204 | if (mfPkg.strings[locale] && mfPkg.strings[locale][key]) 205 | message = mfPkg.strings[locale][key]; 206 | else if (mfPkg.strings[mfPkg.native][key]) 207 | message = mfPkg.strings[mfPkg.native][key]; 208 | else 209 | message = message || key; 210 | 211 | // If loaded from database (only when mfExtract/All.js exists) 212 | if (Meteor.isServer && _.isObject(message)) 213 | message = message.text; 214 | 215 | // cmessage = mfPkg.compiled[locale][key] = mf.compile(message); 216 | cmessage = mfPkg.compiled[locale][key] = new IntlMessageFormat(message, locale, formats); 217 | } 218 | 219 | var formatted = key; 220 | try { 221 | formatted = cmessage.format(params); 222 | } 223 | catch(err) { 224 | log.warn("Error formatting "+key+" in locale "+locale+": "+err.message); 225 | if (locale !== mfPkg.native) { 226 | // Retry in native language 227 | formatted = mfcall(key, params, message, mfPkg.native); 228 | } else { 229 | // Give up 230 | } 231 | } 232 | 233 | return formatted; 234 | } 235 | 236 | // Could make this completely private but useful for plugins to use 237 | var Event = msgfmt._Event = new EventEmitter(); 238 | _.each(['on','once','addListener','removeListener','removeAllListeners'], function(method) { 239 | msgfmt[method] = _.bind(Event[method], Event); 240 | }); 241 | -------------------------------------------------------------------------------- /packages/core/lib/sanitization.js: -------------------------------------------------------------------------------- 1 | // djedi:sanitize-html provides sanitizeHtml 2 | 3 | var santizerPresets = {}; 4 | 5 | msgfmt.addSantizerPreset = function(presetName, optionsOrFunc) { 6 | santizerPresets[presetName] = optionsOrFunc; 7 | } 8 | 9 | msgfmt.sanitizeHTML = function(text, preset) { 10 | // Default sanitization options: [ 'b', 'i', 'em', 'strong', 'a' ] and a['href'] 11 | if (preset === true || preset === 1) 12 | return sanitizeHtml(text); 13 | 14 | if (!santizerPresets[preset]) 15 | throw new Error("[msgfmt] No such _html sanitizer preset '" + preset + "'"); 16 | 17 | // Options as per https://github.com/punkave/sanitize-html 18 | if (typeof preset === 'object') 19 | return sanitizeHtml(text, santizerPresets[preset]); 20 | 21 | // User supplied, named sanitization function 22 | if (typeof preset === 'function') 23 | return santizerPresets[preset](text); 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/mk: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cat lib/messageformat.js/locale/* > lib/mfPkg/locale-all.js 4 | -------------------------------------------------------------------------------- /packages/core/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "msgfmt:core", 3 | version: "2.0.0-preview.24", 4 | summary: "MessageFormat i18n support, the Meteor way", 5 | git: "https://github.com/gadicc/meteor-messageformat.git", 6 | documentation: 'README.md' 7 | }); 8 | 9 | Package.registerBuildPlugin({ 10 | name: 'msgfmt', 11 | sources: [ 'buildPlugin.js' ] 12 | }); 13 | 14 | var client = 'client'; 15 | var server = 'server'; 16 | var both = [ client, server ]; 17 | 18 | Package.onUse(function (api) { 19 | api.versionsFrom("METEOR@1.0.1"); 20 | 21 | // common deps 22 | api.use([ 23 | 'underscore', 24 | 'ddp', 25 | 'mongo@1.0.4', 26 | 'meteorhacks:inject-initial@1.0.3', 27 | 'jag:pince@0.0.6', 28 | 'raix:eventemitter@0.1.2' 29 | ], both); 30 | 31 | // client deps 32 | api.use([ 33 | // core MDG packages 34 | 'session', 35 | 'tracker', 36 | 'reactive-var', 37 | 'ddp', 38 | // core 3rd party packages 39 | 'jquery', 40 | // MDG maintained non-core 41 | 'amplify@1.0.0' 42 | ], client); 43 | 44 | // Blaze support 45 | api.use(['templating', 'spacebars'], client, { weak: true }); 46 | 47 | // For msgfmt.storeUserLocale == true. 48 | api.use('accounts-base', client, { weak: true }); 49 | 50 | // For v0 warning check 51 | api.use('gadicohen:messageformat@0.0.1', server, { weak: true }); 52 | 53 | // api.use('browser-policy', 'server', { /* weak: true */ }); 54 | 55 | //api.use(['ui', 'spacebars', 'htmljs'], 'client'); 56 | 57 | // server deps 58 | api.use([ 59 | 'check', 60 | 'webapp' 61 | ], server); 62 | 63 | // TODO, locales by demand, respect namespacing by wrapping files ourselves 64 | api.addFiles([ 65 | 'upstream/intl-messageformat/dist/intl-messageformat-with-locales.js' 66 | ], both) 67 | 68 | /* --- core package code --- */ 69 | 70 | api.addFiles('lib/msgfmt.js', both); 71 | api.addFiles('lib/msgfmt-server.js', server); 72 | api.addFiles('lib/msgfmt-client.js', client); 73 | 74 | // TODO, on cordova, need to bundle all languages 75 | 76 | /* --- integrations --- */ 77 | 78 | api.addFiles('lib/msgfmt-client-integrations.js', client); 79 | 80 | api.use('cmather:handlebars-server@2.0.0', 'server', { weak: true }); 81 | api.addFiles('lib/msgfmt-server-integrations.js', server); 82 | 83 | /* --- for msgfmt.sanitizeHTML() --- */ 84 | 85 | api.use('djedi:sanitize-html@1.11.2', server); 86 | api.use('djedi:sanitize-html-client@1.11.2-0', client); 87 | api.addFiles('lib/sanitization.js', both); 88 | 89 | /* --- exports --- */ 90 | 91 | api.export(['mfPkg', 'mf', 'msgfmt'], both); 92 | }); 93 | 94 | Package.onTest(function(api) { 95 | api.use('tinytest'); 96 | api.use('msgfmt:core'); 97 | api.use('underscore'); 98 | api.use('http', 'server'); 99 | api.use('browser-policy', server); 100 | api.use('tracker', client); 101 | 102 | api.addFiles('tests/tests-client.js', client); 103 | api.addFiles('tests/tests-server.js', server); 104 | 105 | api.addFiles('tests/integrations/handlebars-server.handlebars', server); 106 | api.addFiles('tests/tests-server-integrations.js', server); 107 | api.use('cmather:handlebars-server@2.0.0', 'server'); 108 | 109 | }); 110 | -------------------------------------------------------------------------------- /packages/core/smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messageformat", 3 | "description": "MessageFormat support, i18n the Meteor way", 4 | "homepage": "https://github.com/gadicohen/meteor-messageformat", 5 | "author": "Gadi Cohen ", 6 | "version": "0.0.43", 7 | "git": "https://github.com/gadicohen/meteor-messageformat.git", 8 | "packages": { 9 | "headers": "0.0.24" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tests/integrations/handlebars-server.handlebars: -------------------------------------------------------------------------------- 1 | {{mf 'hello' 'Hello there, {NAME}' NAME=NAME LOCALE=LOCALE}} -------------------------------------------------------------------------------- /packages/core/tests/tests-client.js: -------------------------------------------------------------------------------- 1 | window.mfPkg = mfPkg; 2 | mfPkg.setBodyDir = false; 3 | Package['jag:pince'].Logger.setLevel('msgfmt', 'trace'); 4 | mfPkg.init('en'); 5 | 6 | Tinytest.addAsync('msgfmt:core - setLocale - sets connectionLocale on server', function(test, complete) { 7 | var locale = 'en'; 8 | 9 | mfPkg.setLocale(locale); 10 | 11 | Meteor.call('getConnectionLocale', function(error, connectionLocale) { 12 | test.isUndefined(error); 13 | test.equal(connectionLocale, locale); 14 | complete(); 15 | }); 16 | }); 17 | 18 | function reset() { 19 | mfPkg.strings = {}; 20 | mfPkg.compiled = {}; 21 | mfPkg.meta = {}; 22 | mfPkg.lastSync = {}; 23 | mfPkg._initialFetches = []; 24 | mfPkg._loadingLocale = false; 25 | mfPkg._locale.set(undefined); 26 | mfPkg._lang.set(undefined); 27 | mfPkg._dir.set(undefined); 28 | } 29 | 30 | Tinytest.addAsync('msgfmt:core - sendPolicy:all', function(test, complete) { 31 | reset(); 32 | mfPkg.waitOnLoaded = true; 33 | 34 | mfPkg.setLocale('en'); 35 | var handle = Tracker.autorun(function() { 36 | if (mfPkg.locale() === 'en') { 37 | // Even though we didn't setLocale("he") yet, it was preloaded 38 | test.equal(mf('test', null, null, 'he'), 'Test - Hebrew'); 39 | handle.stop(); 40 | complete(); 41 | } 42 | }); 43 | }); 44 | 45 | Tinytest.addAsync('msgfmt:core - backcompat - mfPkg.ready(func) on startup', function(test, complete) { 46 | mfPkg.ready(function() { 47 | test.ok(true); 48 | complete(); 49 | }); 50 | }); 51 | 52 | Tinytest.addAsync('msgfmt:core - backcompat - mfPkg.ready() waitOn dep', function(test, complete) { 53 | Tracker.autorun(function(c) { 54 | var ready = mfPkg.ready(); 55 | test.isTrue(ready); 56 | c.stop(); 57 | complete(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/core/tests/tests-server-integrations.js: -------------------------------------------------------------------------------- 1 | Tinytest.add('msgfmt:core - integrations - cmather:handlebars-server', function(test) { 2 | var out = Handlebars.templates['handlebars-server']({ 3 | NAME: 'Chris', 4 | LOCALE: 'en_US' 5 | }); 6 | test.equal(out, 'Hello there, Chris'); 7 | }); -------------------------------------------------------------------------------- /packages/core/tests/tests-server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Code supporting client-side and hybrid tests 3 | */ 4 | 5 | Meteor.methods({ 6 | getConnectionLocale: function() { 7 | return this.connection.locale; 8 | } 9 | }); 10 | 11 | /* 12 | * Server side tests - helpers 13 | */ 14 | 15 | function getInjected(content) { 16 | var match = /