Repository: lingui/js-lingui
Pull request #249 (2018-07-23 05:41:26)

Add support for locales (aka culture)
Closes #170 

codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=h1) Report
> Merging [#249](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=desc) into [stable-2.x](https://codecov.io/gh/lingui/js-lingui/commit/c193f4040af4229cb70d0eda31a069d97fe874a5?src=pr&el=desc) will **increase** coverage by `0.06%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/249/graphs/tree.svg?token=CuJ1VD5BY5&width=650&height=150&src=pr)](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=tree)

```diff
@@              Coverage Diff               @@
##           stable-2.x     #249      +/-   ##
==============================================
+ Coverage       91.33%   91.39%   +0.06%     
==============================================
  Files              41       41              
  Lines             946      953       +7     
==============================================
+ Hits              864      871       +7     
  Misses             82       82
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/core/src/index.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvaW5kZXguanM=) | `100% <ø> (ø)` | :arrow_up: |
| [packages/react/src/Select.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvcmVhY3Qvc3JjL1NlbGVjdC5qcw==) | `100% <ø> (ø)` | :arrow_up: |
| [packages/react/src/I18nProvider.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvcmVhY3Qvc3JjL0kxOG5Qcm92aWRlci5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [packages/react/src/createFormat.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvcmVhY3Qvc3JjL2NyZWF0ZUZvcm1hdC5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [packages/core/src/context.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvY29udGV4dC5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [packages/core/src/i18n.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvaTE4bi5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [packages/core/src/formats.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvZm9ybWF0cy5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [packages/core/src/select.js](https://codecov.io/gh/lingui/js-lingui/pull/249/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29yZS9zcmMvc2VsZWN0Lmpz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=footer). Last update [c193f40...dabe782](https://codecov.io/gh/lingui/js-lingui/pull/249?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.gitignore b/.gitignore
index 14e36d81..d76bbaab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,5 @@ results/
 junit.xml
 
 __pycache__/
+
+.DS_Store
diff --git a/appveyor.yml b/appveyor.yml
index b5014269..3c1cc7b0 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -8,6 +8,9 @@ install:
   # install modules
   - yarn install
   - yarn global add yalc
+  - yarn add full-icu -W
+  - set NODE_ICU_DATA=C:\projects\js-lingui\node_modules\full-icu
+  - setx NODE_ICU_DATA "C:\projects\js-lingui\node_modules\full-icu"
 
 # Post-install test scripts.
 test_script:
@@ -15,7 +18,7 @@ test_script:
   - node --version
   - yarn --version
   # run tests
-  - node scripts/test.js
+  - node --icu-data-dir=C:\projects\js-lingui\node_modules\full-icu scripts/test.js
 
 # Don't actually build.
 build: off
diff --git a/circle.yml b/circle.yml
index febaf59e..f500d539 100644
--- a/circle.yml
+++ b/circle.yml
@@ -9,6 +9,13 @@ aliases:
   - &yarn-install
       name: Install dependencies
       command: yarn install --no-progress
+  - &yarn-install-tools
+      name: Install build tools
+      command: sudo yarn global add codecov yalc
+  - &tests
+      command: node scripts/test.js --skip-build
+      environment:
+        NODE_ICU_DATA: /home/circleci/project/node_modules/full-icu
 
 jobs:
   build:
@@ -17,6 +24,7 @@ jobs:
 
     steps:
       - checkout
+
       - restore-cache: *restore-cache
       - run: *yarn-install
 
@@ -25,7 +33,7 @@ jobs:
           paths:
               - node_modules
 
-      - run: node scripts/build/
+      - run: node ./scripts/build/
 
       - persist_to_workspace:
           root: build
@@ -41,17 +49,9 @@ jobs:
       - attach-workspace: *attach-workspace
       - restore-cache: *restore-cache
       - run: *yarn-install
-
-      - run:
-          name: Install build tools
-          command: sudo yarn global add codecov yalc
-
-      - run: node scripts/test.js --skip-build
-
-      - run:
-          name: Send coverage report
-          command: codecov
-
+      - run: *yarn-install-tools
+      - run: *tests
+      - run: codecov
       - store_test_results:
           path: ./results/
           when: always
@@ -65,12 +65,8 @@ jobs:
       - attach-workspace: *attach-workspace
       - restore-cache: *restore-cache
       - run: *yarn-install
-
-      - run:
-          name: Install build tools
-          command: sudo yarn global add codecov yalc
-
-      - run: node scripts/test.js --skip-build
+      - run: *yarn-install-tools
+      - run: *tests
 
   publish:
     docker:
diff --git a/package.json b/package.json
index 3db30240..6ecbca06 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "filesize": "^3.5.11",
     "flow-bin": "^0.75.0",
     "fs-extra": "^6.0.1",
+    "full-icu": "^1.2.1",
     "glob": "^7.1.2",
     "gzip-size": "^4.1.0",
     "husky": "^0.14.3",
diff --git a/packages/core/src/context.js b/packages/core/src/context.js
index c3eb5144..3a4903e2 100644
--- a/packages/core/src/context.js
+++ b/packages/core/src/context.js
@@ -1,17 +1,27 @@
 // @flow
+import type { Locales } from "./i18n"
 import { date, number } from "./formats"
 import { isString, isFunction } from "./essentials"
 
-const defaultFormats = (language, languageData = {}, formats = {}) => {
+type IntlType = {|
+  DateTimeFormat: Function,
+  NumberFormat: Function
+|}
+
+declare var Intl: IntlType
+
+const defaultFormats = (language, locales, languageData = {}, formats = {}) => {
+  locales = locales || language
   const { plurals } = languageData
   const style = format =>
     isString(format) ? formats[format] || { style: format } : format
-
   const replaceOctothorpe = (value, message) => {
     return ctx => {
       const msg = isFunction(message) ? message(ctx) : message
       const norm = Array.isArray(msg) ? msg : [msg]
-      return norm.map(m => (isString(m) ? m.replace("#", value) : m))
+      const formatter = new Intl.NumberFormat(locales)
+      const valueStr = formatter.format(value)
+      return norm.map(m => (isString(m) ? m.replace("#", valueStr) : m))
     }
   }
 
@@ -28,9 +38,9 @@ const defaultFormats = (language, languageData = {}, formats = {}) => {
 
     select: (value, rules) => rules[value] || rules.other,
 
-    number: (value, format) => number(language, style(format))(value),
+    number: (value, format) => number(locales, style(format))(value),
 
-    date: (value, format) => date(language, style(format))(value),
+    date: (value, format) => date(locales, style(format))(value),
 
     undefined: value => value
   }
@@ -42,13 +52,14 @@ const defaultFormats = (language, languageData = {}, formats = {}) => {
  * argument type.
  *
  * @param language     - Language of message
+ * @param locales      - Locales to be used when formatting the numbers or dates
  * @param values       - Parameters for variable interpolation
  * @param languageData - Language data (e.g: plurals)
  * @param formats - Custom format styles
  * @returns {function(string, string, any)}
  */
-function context({ language, values, formats, languageData }: Object) {
-  const formatters = defaultFormats(language, languageData, formats)
+function context({ language, locales, values, formats, languageData }: Object) {
+  const formatters = defaultFormats(language, locales, languageData, formats)
 
   const ctx = (name: string, type: string, format: any) => {
     const value = values[name]
@@ -63,12 +74,14 @@ function context({ language, values, formats, languageData }: Object) {
 export function interpolate(
   translation: Function,
   language: string,
+  locales: ?Locales,
   languageData: Object
 ) {
   return (values: Object, formats?: Object = {}) => {
     const message = translation(
       context({
         language,
+        locales,
         languageData,
         formats,
         values
diff --git a/packages/core/src/formats.js b/packages/core/src/formats.js
index 89d76b24..66aa7a76 100644
--- a/packages/core/src/formats.js
+++ b/packages/core/src/formats.js
@@ -1,8 +1,10 @@
 // @flow
-type NumberFormat = string | {}
-type DateFormat = string | {}
+import type { Locales } from "./i18n"
 
-type IntlType = {|
+export type NumberFormat = string | {}
+export type DateFormat = string | {}
+
+export type IntlType = {|
   DateTimeFormat: Function,
   NumberFormat: Function
 |}
@@ -10,10 +12,10 @@ type IntlType = {|
 declare var Intl: IntlType
 
 export function date(
-  language: string,
+  locales: Locales,
   format?: DateFormat = {}
 ): (value: string | Date) => string {
-  const formatter = new Intl.DateTimeFormat(language, format)
+  const formatter = new Intl.DateTimeFormat(locales, format)
   return value => {
     if (typeof value === "string") value = new Date(value)
     return formatter.format(value)
@@ -21,9 +23,9 @@ export function date(
 }
 
 export function number(
-  language: string,
+  locales: Locales,
   format?: NumberFormat = {}
 ): (value: number) => string {
-  const formatter = new Intl.NumberFormat(language, format)
+  const formatter = new Intl.NumberFormat(locales, format)
   return value => formatter.format(value)
 }
diff --git a/packages/core/src/i18n.js b/packages/core/src/i18n.js
index 59a170ff..33d0fd57 100644
--- a/packages/core/src/i18n.js
+++ b/packages/core/src/i18n.js
@@ -10,6 +10,8 @@ type MessageOptions = {|
   formats?: Object
 |}
 
+type Locales = string | string[]
+
 type LanguageData = {
   plurals?: Function
 }
@@ -25,6 +27,7 @@ type Catalogs = { [key: string]: Catalog }
 
 type setupI18nProps = {
   language?: string,
+  locales?: Locales,
   catalogs?: Catalogs,
   development?: Object
 }
@@ -39,6 +42,7 @@ function getMessages(catalog) {
 
 class I18n {
   _language: string
+  _locales: ?Locales
 
   // Message catalogs
   _catalogs: Catalogs
@@ -78,6 +82,10 @@ class I18n {
     return this._language
   }
 
+  get locales(): ?Locales {
+    return this._locales
+  }
+
   get messages(): Messages {
     return this._activeMessages
   }
@@ -140,7 +148,7 @@ class I18n {
     this._cacheActiveLanguage()
   }
 
-  activate(language: string) {
+  activate(language: string, locales?: Locales) {
     if (!language) return
 
     if (process.env.NODE_ENV !== "production") {
@@ -150,12 +158,14 @@ class I18n {
     }
 
     this._language = language
+    this._locales = locales
     this._cacheActiveLanguage()
   }
 
-  use(language: string) {
+  use(language: string, locales?: Locales) {
     return setupI18n({
       language,
+      locales,
       catalogs: this._catalogs,
       development: this._dev
     })
@@ -176,10 +186,12 @@ class I18n {
     }
 
     if (typeof translation !== "function") return translation
-    return interpolate(translation, this.language, this.languageData)(
-      values,
-      formats
-    )
+    return interpolate(
+      translation,
+      this.language,
+      this.locales,
+      this.languageData
+    )(values, formats)
   }
 
   pluralForm(
@@ -199,7 +211,7 @@ function setupI18n(params?: setupI18nProps = {}): I18n {
   }
 
   if (params.catalogs) i18n.load(params.catalogs)
-  if (params.language) i18n.activate(params.language)
+  if (params.language) i18n.activate(params.language, params.locales)
 
   return i18n
 }
@@ -207,4 +219,4 @@ function setupI18n(params?: setupI18nProps = {}): I18n {
 const i18n = setupI18n()
 
 export { setupI18n, i18n }
-export type { MessageOptions, Catalog, Catalogs, LanguageData, I18n }
+export type { MessageOptions, Catalog, Catalogs, LanguageData, I18n, Locales }
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 63f7d142..feba0a3f 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -7,7 +7,8 @@ export type {
   Catalogs,
   MessageOptions,
   LanguageData,
-  I18n
+  I18n,
+  Locales
 } from "./i18n"
 
 export const i18nMark = (id: string) => id
diff --git a/packages/core/src/select.js b/packages/core/src/select.js
index 935253f5..92feba20 100644
--- a/packages/core/src/select.js
+++ b/packages/core/src/select.js
@@ -1,5 +1,9 @@
 /* @flow */
 
+import { number } from "./formats"
+import type { NumberFormat } from "./formats"
+import type { Locales, I18n } from "./i18n"
+
 type PluralForms = {
   zero?: string,
   one?: string,
@@ -11,21 +15,32 @@ type PluralForms = {
 
 type PluralProps = {
   value: number,
-  offset?: number
+  offset?: number,
+  locales?: Locales,
+  format?: NumberFormat
 } & PluralForms
 
-const _plural = type => (i18n: any) => ({
+declare var Intl: {
+  NumberFormat: any
+}
+
+const _plural = type => (i18n: I18n) => ({
   value,
   offset = 0,
+  locales,
+  format,
   other,
   ...pluralForms
 }: PluralProps): string => {
+  if (locales === undefined) locales = i18n.locales || i18n.language
+
   const diff = value - offset
+  const diffAsString = number(locales, format)(diff)
   const translation =
     pluralForms[value.toString()] || // exact match
     pluralForms[i18n.pluralForm(diff, type)] || // plural form
     other // fallback
-  return translation.replace("#", diff.toString())
+  return translation.replace("#", diffAsString)
 }
 
 const plural = _plural("cardinal")
diff --git a/packages/react/src/I18nProvider.js b/packages/react/src/I18nProvider.js
index 52f660ab..934989f2 100644
--- a/packages/react/src/I18nProvider.js
+++ b/packages/react/src/I18nProvider.js
@@ -1,15 +1,15 @@
 /* @flow */
 import * as React from "react"
-import type { Node } from "react"
 import PropTypes from "prop-types"
 import hashSum from "hash-sum"
 
 import { setupI18n } from "@lingui/core"
-import type { I18n, Catalogs } from "@lingui/core"
+import type { I18n, Catalogs, Locales } from "@lingui/core"
 
 export type I18nProviderProps = {
-  children?: Node,
+  children?: React.Node,
   language: string,
+  locales?: Locales,
   catalogs?: Catalogs,
   i18n?: I18n,
 
@@ -41,12 +41,13 @@ export function LinguiPublisher(i18n: I18n) {
 
     update({
       catalogs,
-      language
-    }: { catalogs?: Catalogs, language?: string } = {}) {
-      if (!catalogs && !language) return
+      language,
+      locales
+    }: { catalogs?: Catalogs, language?: string, locales?: string } = {}) {
+      if (!catalogs && !language && !locales) return
 
       if (catalogs) i18n.load(catalogs)
-      if (language) i18n.activate(language)
+      if (language) i18n.activate(language, locales)
 
       this.i18nHash = hashSum([i18n.language, i18n.messages])
 
@@ -71,20 +72,25 @@ export default class I18nProvider extends React.Component<I18nProviderProps> {
 
   constructor(props: I18nProviderProps) {
     super(props)
-    const { language, catalogs } = props
+    const { language, locales, catalogs } = props
     const i18n =
       props.i18n ||
       setupI18n({
         language,
+        locales,
         catalogs
       })
     this.linguiPublisher = new LinguiPublisher(i18n)
   }
 
   componentDidUpdate(prevProps: I18nProviderProps) {
-    const { language, catalogs } = this.props
-    if (language !== prevProps.language || catalogs !== prevProps.catalogs) {
-      this.linguiPublisher.update({ language, catalogs })
+    const { language, locales, catalogs } = this.props
+    if (
+      language !== prevProps.language ||
+      locales !== prevProps.locales ||
+      catalogs !== prevProps.catalogs
+    ) {
+      this.linguiPublisher.update({ language, catalogs, locales })
     }
   }
 
diff --git a/packages/react/src/Select.js b/packages/react/src/Select.js
index 69d52483..733327eb 100644
--- a/packages/react/src/Select.js
+++ b/packages/react/src/Select.js
@@ -1,5 +1,6 @@
 // @flow
 import * as React from "react"
+import type { Locales } from "@lingui/core"
 import withI18n from "./withI18n"
 import type { withI18nProps } from "./withI18n"
 
@@ -14,7 +15,8 @@ type PluralProps = {
   two?: any,
   few?: any,
   many?: any,
-  other: any
+  other: any,
+  locales?: Locales
 } & withI18nProps &
   RenderProps
 
@@ -69,8 +71,8 @@ const PluralFactory = (ordinal = false) => {
           return acc
         },
         {
-          value: parseInt(value),
-          offset: parseInt(offset)
+          value: Number(value),
+          offset: Number(offset)
         }
       )
 
diff --git a/packages/react/src/createFormat.js b/packages/react/src/createFormat.js
index 0990654e..622ce3c2 100644
--- a/packages/react/src/createFormat.js
+++ b/packages/react/src/createFormat.js
@@ -1,6 +1,7 @@
 // @flow
 import * as React from "react"
 
+import type { Locales } from "@lingui/core"
 import type { RenderProps } from "./Render"
 import Render from "./Render"
 
@@ -8,13 +9,14 @@ type FormatProps<V, FormatOptions> = {
   value: V,
   format?: FormatOptions,
   i18n: {
-    language: string
+    language: string,
+    locales?: Locales
   }
 } & RenderProps
 
 function createFormat<V, FormatOptions>(
   formatFunction: (
-    language: string,
+    language: Locales,
     format?: FormatOptions
   ) => (value: V) => string
 ) {
@@ -25,7 +27,7 @@ function createFormat<V, FormatOptions>(
     className,
     render
   }: FormatProps<V, FormatOptions>) {
-    const formatter = formatFunction(i18n.language, format)
+    const formatter = formatFunction(i18n.locales || i18n.language, format)
     return (
       <Render className={className} render={render} value={formatter(value)} />
     )
diff --git a/scripts/build/results.json b/scripts/build/results.json
index 0dbc1d67..cc62efaf 100644
--- a/scripts/build/results.json
+++ b/scripts/build/results.json
@@ -1,20 +1,20 @@
 {
   "bundleSizes": {
     "@lingui/core.development.js (NODE_DEV)": {
-      "size": 13273,
-      "gzip": 3521
+      "size": 13974,
+      "gzip": 3692
     },
     "@lingui/core.production.min.js (NODE_PROD)": {
-      "size": 5552,
-      "gzip": 1833
+      "size": 5741,
+      "gzip": 1892
     },
     "@lingui/react.development.js (NODE_DEV)": {
-      "size": 18003,
-      "gzip": 5011
+      "size": 18214,
+      "gzip": 5063
     },
     "@lingui/react.production.min.js (NODE_PROD)": {
-      "size": 8511,
-      "gzip": 3014
+      "size": 8598,
+      "gzip": 3084
     }
   }
 }
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 17b8b466..259fd3c9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2590,6 +2590,10 @@ fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
     mkdirp ">=0.5 0"
     rimraf "2"
 
+full-icu@^1.2.1:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/full-icu/-/full-icu-1.2.1.tgz#28d7f1caafb14cac262364ca584fe7f2044a4ab2"
+
 function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"


Test patch:
diff --git a/examples/webpack-react/src/Usecases/__snapshots__/Children.test.js.snap b/examples/webpack-react/src/Usecases/__snapshots__/Children.test.js.snap
index b3cbb6a7..81e10ddc 100644
--- a/examples/webpack-react/src/Usecases/__snapshots__/Children.test.js.snap
+++ b/examples/webpack-react/src/Usecases/__snapshots__/Children.test.js.snap
@@ -68,6 +68,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -127,6 +128,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -186,6 +188,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -250,6 +253,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -328,6 +332,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -412,6 +417,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -487,6 +493,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -567,6 +574,7 @@ exports[`Children should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: cs,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
diff --git a/examples/webpack-react/src/Usecases/__snapshots__/Formats.test.js.snap b/examples/webpack-react/src/Usecases/__snapshots__/Formats.test.js.snap
index 02f92bd2..9411e68d 100644
--- a/examples/webpack-react/src/Usecases/__snapshots__/Formats.test.js.snap
+++ b/examples/webpack-react/src/Usecases/__snapshots__/Formats.test.js.snap
@@ -45,6 +45,7 @@ exports[`Formats should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: en,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
@@ -97,6 +98,7 @@ exports[`Formats should render 1`] = `
                       loadLanguageData: [Function],
                     },
                     _language: en,
+                    _locales: undefined,
                     plural: [Function],
                     select: [Function],
                     selectOrdinal: [Function],
diff --git a/packages/core/src/dev/compile.test.js b/packages/core/src/dev/compile.test.js
index e27c3572..6a66466a 100644
--- a/packages/core/src/dev/compile.test.js
+++ b/packages/core/src/dev/compile.test.js
@@ -13,8 +13,8 @@ describe("compile", function() {
     }
   }
 
-  const prepare = translation =>
-    interpolate(compile(translation), "en", englishPlurals)
+  const prepare = (translation, language, locales) =>
+    interpolate(compile(translation), language || "en", locales, englishPlurals)
 
   it("should compile static message", function() {
     const cache = compile("Static message")
@@ -60,27 +60,51 @@ describe("compile", function() {
     expect(cache({ value: "n/a" })).toEqual("They")
   })
 
-  it("should compile custom format", function() {
-    const number = prepare("{value, number}")
-    expect(number({ value: 0.1 })).toEqual("0.1")
+  const testVector = [
+    ["en", null, "0.1", "10%", "20%", "3/4/2017", "€0.10", "€1.00"],
+    ["fr", null, "0,1", "10 %", "20 %", "04/03/2017", "0,10 €", "1,00 €"],
+    ["fr", "fr-CH", "0,1", "10%", "20%", "04.03.2017", "0.10 €", "1.00 €"]
+  ]
+  testVector.forEach(tc => {
+    const [
+      language,
+      locales,
+      expectedNumber,
+      expectedPercent1,
+      expectedPercent2,
+      expectedDate,
+      expectedCurrency1,
+      expectedCurrency2
+    ] = tc
 
-    const percent = prepare("{value, number, percent}")
-    expect(percent({ value: 0.1 })).toEqual("10%")
-    expect(percent({ value: 0.2 })).toEqual("20%")
+    it(
+      "should compile custom format for language=" +
+        language +
+        " and locales=" +
+        locales,
+      function() {
+        const number = prepare("{value, number}", language, locales)
+        expect(number({ value: 0.1 })).toEqual(expectedNumber)
 
-    const now = new Date("3/4/2017")
-    const date = prepare("{value, date}")
-    expect(date({ value: now })).toEqual("3/4/2017")
+        const percent = prepare("{value, number, percent}", language, locales)
+        expect(percent({ value: 0.1 })).toEqual(expectedPercent1)
+        expect(percent({ value: 0.2 })).toEqual(expectedPercent2)
 
-    const formats = {
-      currency: {
-        style: "currency",
-        currency: "EUR",
-        minimumFractionDigits: 2
+        const now = new Date("3/4/2017")
+        const date = prepare("{value, date}", language, locales)
+        expect(date({ value: now })).toEqual(expectedDate)
+
+        const formats = {
+          currency: {
+            style: "currency",
+            currency: "EUR",
+            minimumFractionDigits: 2
+          }
+        }
+        const currency = prepare("{value, number, currency}", language, locales)
+        expect(currency({ value: 0.1 }, formats)).toEqual(expectedCurrency1)
+        expect(currency({ value: 1 }, formats)).toEqual(expectedCurrency2)
       }
-    }
-    const currency = prepare("{value, number, currency}")
-    expect(currency({ value: 0.1 }, formats)).toEqual("€0.10")
-    expect(currency({ value: 1 }, formats)).toEqual("€1.00")
+    )
   })
 })
diff --git a/packages/core/src/i18n.test.js b/packages/core/src/i18n.test.js
index b57158bf..3de3bdc7 100644
--- a/packages/core/src/i18n.test.js
+++ b/packages/core/src/i18n.test.js
@@ -156,6 +156,50 @@ describe("I18n", function() {
     expect(i18n._("Hello")).toEqual("Hello")
   })
 
+  it(".use should return new i18n object with switched locales", function() {
+    const i18n = setupI18n({
+      language: "en",
+      locales: "en-UK"
+    })
+
+    // change locales locally
+    const ar = i18n.use("ar")
+    expect(
+      ar.plural({
+        value: 2,
+        "=0": "لا كتاب",
+        other: "# الكتب"
+      })
+    ).toEqual("٢ الكتب")
+
+    const uae = i18n.use("ar", "en-UK")
+    expect(
+      uae.plural({
+        value: 2,
+        "=0": "لا كتاب",
+        other: "# الكتب"
+      })
+    ).toEqual("2 الكتب")
+
+    const uae2 = i18n.use("ar", ["unknown-locale", "en-UK"])
+    expect(
+      uae2.plural({
+        value: 2,
+        "=0": "لا كتاب",
+        other: "# الكتب"
+      })
+    ).toEqual("2 الكتب")
+
+    // global locales hasn't changed
+    expect(
+      i18n.plural({
+        value: 2,
+        "=0": "no book",
+        other: "# books"
+      })
+    ).toEqual("2 books")
+  })
+
   it("._ should format message from catalog", function() {
     const messages = {
       Hello: "Salut",
diff --git a/packages/core/src/select.test.js b/packages/core/src/select.test.js
index 4d1cc38c..eeb6f445 100644
--- a/packages/core/src/select.test.js
+++ b/packages/core/src/select.test.js
@@ -12,8 +12,20 @@ describe("plural", function() {
     }
   })
 
-  it("should convert to message format string", function() {
+  const i18nDe = setupI18n({
+    language: "de",
+    locales: "de-DE",
+    catalogs: {
+      en: {
+        messages: {}
+      }
+    }
+  })
+
+  it.only("should convert to message format string", function() {
     const p = plural(i18n)
+    const pDe = plural(i18nDe)
+
     expect(
       p({
         value: 1,
@@ -49,6 +61,53 @@ describe("plural", function() {
         other: "Their and # books"
       })
     ).toEqual("Their and 4 books")
+
+    expect(
+      p({
+        value: 0.1234,
+        other: "# completed",
+        format: { style: "percent" }
+      })
+    ).toEqual("12% completed")
+
+    expect(
+      p({
+        value: 1,
+        other: "# كتاب",
+        locales: "en-UK"
+      })
+    ).toEqual("1 كتاب")
+
+    expect(
+      p({
+        value: 1,
+        other: "لدي # كتاب",
+        locales: "ar-AS"
+      })
+    ).toEqual("لدي ١ كتاب")
+
+    expect(
+      pDe({
+        value: 1000,
+        other: "# Bücher"
+      })
+    ).toEqual("1.000 Bücher")
+
+    expect(
+      pDe({
+        value: 1000,
+        other: "# Bücher",
+        locales: "en-UK"
+      })
+    ).toEqual("1,000 Bücher")
+
+    expect(
+      pDe({
+        value: 1000,
+        other: "# Bücher",
+        locales: ["unknown-locale", "en-UK"]
+      })
+    ).toEqual("1,000 Bücher")
   })
 })
 
diff --git a/packages/react/src/DateFormat.test.js b/packages/react/src/DateFormat.test.js
index 0b8dc3dd..1000f825 100644
--- a/packages/react/src/DateFormat.test.js
+++ b/packages/react/src/DateFormat.test.js
@@ -5,8 +5,15 @@ import { mount } from "enzyme"
 import { DateFormat, NumberFormat } from "@lingui/react"
 
 describe("DateFormat", function() {
-  const languageContext = code => ({
-    context: { linguiPublisher: { i18n: { language: code } } }
+  const languageContext = (language, locales) => ({
+    context: {
+      linguiPublisher: {
+        i18n: {
+          language: language,
+          locales: locales
+        }
+      }
+    }
   })
 
   it("should render", function() {
@@ -31,6 +38,24 @@ describe("DateFormat", function() {
     expect(dateString.text()).toEqual(expectedDate)
   })
 
+  it("should render using the given locales", function() {
+    const now = new Date("2017-06-17:14:00.000Z")
+    const node = mount(
+      <DateFormat value={now} />,
+      languageContext("en", "fr-FR")
+    ).find("Render")
+    expect(node.text()).toEqual("17/06/2017")
+  })
+
+  it("should render using the first recognized locale", function() {
+    const now = new Date("2017-06-17:14:00.000Z")
+    const node = mount(
+      <DateFormat value={now} />,
+      languageContext("en", ["unknown-locale", "fr-FR"])
+    ).find("Render")
+    expect(node.text()).toEqual("17/06/2017")
+  })
+
   it("should render translation inside custom component", function() {
     const now = new Date("2017-06-17:14:00.000Z")
     const [day, month, year] = [
@@ -61,8 +86,10 @@ describe("DateFormat", function() {
 })
 
 describe("NumberFormat", function() {
-  const languageContext = code => ({
-    context: { linguiPublisher: { i18n: { language: code } } }
+  const languageContext = (language, locales) => ({
+    context: {
+      linguiPublisher: { i18n: { language: language, locales: locales } }
+    }
   })
 
   it("should render", function() {
@@ -80,6 +107,36 @@ describe("NumberFormat", function() {
     expect(node.text()).toEqual("€42.00")
   })
 
+  it("should render using the given locales", function() {
+    const node = mount(
+      <NumberFormat
+        value={42000}
+        format={{
+          style: "currency",
+          currency: "EUR",
+          minimumFractionDigits: 2
+        }}
+      />,
+      languageContext("en", "de-DE")
+    ).find("Render")
+    expect(node.text()).toEqual("42.000,00 €")
+  })
+
+  it("should render using the first recognized locale", function() {
+    const node = mount(
+      <NumberFormat
+        value={42000}
+        format={{
+          style: "currency",
+          currency: "EUR",
+          minimumFractionDigits: 2
+        }}
+      />,
+      languageContext("en", ["unknown-locale", "de-DE"])
+    ).find("Render")
+    expect(node.text()).toEqual("42.000,00 €")
+  })
+
   it("should render translation inside custom component", function() {
     const format = {
       style: "currency",
diff --git a/packages/react/src/I18nProvider.test.js b/packages/react/src/I18nProvider.test.js
index a61463e3..b2d20f11 100644
--- a/packages/react/src/I18nProvider.test.js
+++ b/packages/react/src/I18nProvider.test.js
@@ -114,6 +114,10 @@ describe("I18nProvider", function() {
 
     node.setProps({ language: "cs" })
     expect(listener).toBeCalled()
+    listener.mockClear()
+
+    node.setProps({ locales: "cs-CZ" })
+    expect(listener).toBeCalled()
   })
 })
 
@@ -193,7 +197,11 @@ describe("I18nPublisher", function() {
 
     linguiPublisher.update({ language: "fr" })
     expect(listener).toBeCalled()
-    listener.mockReset()
+    listener.mockClear()
+
+    linguiPublisher.update({ locales: "fr-BE" })
+    expect(listener).toBeCalled()
+    listener.mockClear()
 
     linguiPublisher.update({ catalogs: { en: { messages: { id: "hello" } } } })
     expect(listener).toBeCalled()
diff --git a/packages/react/src/Select.test.js b/packages/react/src/Select.test.js
index 9f12c666..1f90045d 100644
--- a/packages/react/src/Select.test.js
+++ b/packages/react/src/Select.test.js
@@ -5,13 +5,14 @@ import { setupI18n } from "@lingui/core"
 import { Select, Plural, SelectOrdinal } from "@lingui/react"
 
 describe("Categories", function() {
-  const i18n = code =>
+  const i18n = (language, locales) =>
     setupI18n({
-      language: code,
-      catalogs: { [code]: {} }
+      language: language,
+      locales: locales,
+      catalogs: { [language]: {} }
     })
-  const languageContext = code => ({
-    context: { linguiPublisher: { i18n: i18n(code) } }
+  const languageContext = (language, locales) => ({
+    context: { linguiPublisher: { i18n: i18n(language, locales) } }
   })
 
   describe("Plural", function() {
@@ -74,6 +75,67 @@ describe("Categories", function() {
       expect(t()).toEqual("5 knih")
     })
 
+    it("should use plural forms based on locales", function() {
+      const node = mount(
+        <Plural value="1000" one="#" other="#" locales="de-DE" />,
+        languageContext("en")
+      )
+
+      const t = () => node.find("Render").text()
+
+      expect(t()).toEqual("1.000")
+
+      node.setProps({ value: 2500 })
+      expect(t()).toEqual("2.500")
+    })
+
+    it("should use plural forms based on locales set on i18n", function() {
+      const node = mount(
+        <Plural value="1000" one="#" other="#" />,
+        languageContext("de", "de-DE")
+      )
+
+      const t = () => node.find("Render").text()
+
+      expect(t()).toEqual("1.000")
+
+      node.setProps({ value: 2500 })
+      expect(t()).toEqual("2.500")
+    })
+
+    it("should use plural forms based on language set on i18n", function() {
+      const node = mount(
+        <Plural value="1000" one="#" other="#" />,
+        languageContext("de")
+      )
+
+      const t = () => node.find("Render").text()
+
+      expect(t()).toEqual("1.000")
+
+      node.setProps({ value: 2500 })
+      expect(t()).toEqual("2.500")
+    })
+
+    it("should use plural forms based on options", function() {
+      const node = mount(
+        <Plural
+          value="0.1234"
+          one="#"
+          other="#"
+          format={{ style: "percent" }}
+        />,
+        languageContext("en")
+      )
+
+      const t = () => node.find("Render").text()
+
+      expect(t()).toEqual("12%")
+
+      node.setProps({ value: 0.2345 })
+      expect(t()).toEqual("23%")
+    })
+
     it("should offset value", function() {
       const node = mount(
         <Plural