Repository: lingui/js-lingui
Pull request #1293 (2022-10-19 20:47:07)

fix: Make pseudolocal use the message AST instead of the key
This is fixing this issue: https://github.com/lingui/js-lingui/issues/1213

I'm not sure why in this PR (https://github.com/lingui/js-lingui/pull/1165) there was a change from the message to the key, but I hope I don't mess anything

Also, I wonder if we should remove the `addDelimitersVariables` & `addDelimitersMacro` and keep only the handling of HTML inside the `pseudoLocalize.ts` code

vercel[bot]: **The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated |
| :--- | :----- | :------ | :------ |
| **js-lingui** | 🔄 Building ([Inspect](https://vercel.com/lingui/js-lingui/5JqHdxRrFP2bx6rP7YeA66wBBoPV)) |  | Oct 19, 2022 at 8:47PM (UTC) |


codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/1293?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) Report
Base: **81.67**% // Head: **82.08**% // Increases project coverage by **`+0.41%`** :tada:
> Coverage data is based on head [(`5b3b6a8`)](https://codecov.io/gh/lingui/js-lingui/pull/1293?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) compared to base [(`da070ba`)](https://codecov.io/gh/lingui/js-lingui/commit/da070ba294435484fde10d82ecb895d2410ec1b7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1293      +/-   ##
==========================================
+ Coverage   81.67%   82.08%   +0.41%     
==========================================
  Files          56       56              
  Lines        1779     1781       +2     
  Branches      495      496       +1     
==========================================
+ Hits         1453     1462       +9     
+ Misses        196      192       -4     
+ Partials      130      127       -3     
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/1293?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui) | Coverage Δ | |
|---|---|---|
| [packages/cli/src/api/compile.ts](https://codecov.io/gh/lingui/js-lingui/pull/1293/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui#diff-cGFja2FnZXMvY2xpL3NyYy9hcGkvY29tcGlsZS50cw==) | `100.00% <100.00%> (+11.66%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/1293?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lingui).


Patch:
diff --git a/packages/cli/src/api/catalog.ts b/packages/cli/src/api/catalog.ts
index 3e13f7eb..4af1ccd6 100644
--- a/packages/cli/src/api/catalog.ts
+++ b/packages/cli/src/api/catalog.ts
@@ -31,6 +31,7 @@ export type ExtractedMessageType = {
   obsolete?: boolean
   flags?: string[]
   context?: string
+  defaults?: string
 }
 
 export type MessageType = ExtractedMessageType & {
@@ -254,50 +255,55 @@ export class Catalog {
 
   getTranslations(locale: string, options: GetTranslationsOptions) {
     const catalogs = this.readAll()
+    const template = this.readTemplate() || {}
+
+
     return R.mapObjIndexed(
       (_value, key) => this.getTranslation(catalogs, locale, key, options),
-      catalogs[locale]
+      { ...template, ...catalogs[locale] },
     )
   }
 
   getTranslation(
-    catalogs: Object,
+    catalogs: AllCatalogsType,
     locale: string,
     key: string,
     { fallbackLocales, sourceLocale }: GetTranslationsOptions
   ) {
-    if (!catalogs[locale].hasOwnProperty(key)) {
+    const catalog = catalogs[locale] || {}
+
+    if (!catalog.hasOwnProperty(key)) {
       console.error(`Message with key ${key} is missing in locale ${locale}`)
     }
 
-    const getTranslation = (locale) => {
+    const getTranslation = (_locale: string) => {
       const configLocales = this.config.locales.join('", "')
-      const localeCatalog = catalogs[locale]
+      const localeCatalog = catalogs[_locale] || {}
 
       if (!localeCatalog) {
         console.warn(`
-        Catalog "${locale}" isn't present in locales config parameter
-        Add "${locale}" to your lingui.config.js:
+        Catalog "${_locale}" isn't present in locales config parameter
+        Add "${_locale}" to your lingui.config.js:
         {
-          locales: ["${configLocales}", "${locale}"]
+          locales: ["${configLocales}", "${_locale}"]
         }
       `)
         return null
       }
       if (!localeCatalog.hasOwnProperty(key)) {
-        console.error(`Message with key ${key} is missing in locale ${locale}`)
+        console.error(`Message with key ${key} is missing in locale ${_locale}`)
         return null
       }
 
-      if (catalogs[locale]) {
-        return catalogs[locale][key].translation
+      if (catalogs[_locale]) {
+        return catalogs[_locale][key].translation
       }
 
       return null
     }
 
-    const getMultipleFallbacks = (locale) => {
-      const fL = fallbackLocales && fallbackLocales[locale]
+    const getMultipleFallbacks = (_locale: string) => {
+      const fL = fallbackLocales && fallbackLocales[_locale]
 
       // some probably the fallback will be undefined, so just search by locale
       if (!fL) return null
@@ -319,16 +325,13 @@ export class Catalog {
       // We search in fallbackLocales as dependent of each locale
       getMultipleFallbacks(locale) ||
       // Get translation in fallbackLocales.default (if any)
-      (fallbackLocales &&
-        fallbackLocales.default &&
-        getTranslation(fallbackLocales.default)) ||
+      (fallbackLocales?.default && getTranslation(fallbackLocales.default as string)) ||
       // Get message default
-      catalogs[locale][key].defaults ||
+      catalog[key]?.defaults ||
       // If sourceLocale is either target locale of fallback one, use key
       (sourceLocale && sourceLocale === locale && key) ||
       (sourceLocale &&
-        fallbackLocales &&
-        fallbackLocales.default &&
+        fallbackLocales?.default &&
         sourceLocale === fallbackLocales.default &&
         key) ||
       // Otherwise no translation is available
@@ -398,7 +401,7 @@ export class Catalog {
     return this.format.read(filename)
   }
 
-  readAll() {
+  readAll(): AllCatalogsType {
     return R.mergeAll(
       this.locales.map((locale) => ({
         [locale]: this.read(locale),
@@ -406,7 +409,7 @@ export class Catalog {
     ) as AllCatalogsType
   }
 
-  readTemplate() {
+  readTemplate(): CatalogType {
     const filename = this.templateFile
     if (!fs.existsSync(filename)) return null
     return this.format.read(filename)
@@ -456,9 +459,9 @@ export class Catalog {
 /**
  * Parse `config.catalogs` and return a list of configured Catalog instances.
  */
-export function getCatalogs(config: LinguiConfig) {
+export function getCatalogs(config: LinguiConfig): Catalog[] {
   const catalogsConfig = config.catalogs
-  const catalogs = []
+  const catalogs: Catalog[] = []
 
   catalogsConfig.forEach((catalog) => {
     // Validate that `catalogPath` doesn't end with trailing slash
diff --git a/packages/cli/src/api/compile.ts b/packages/cli/src/api/compile.ts
index 0ebd8add..42e0882c 100644
--- a/packages/cli/src/api/compile.ts
+++ b/packages/cli/src/api/compile.ts
@@ -5,7 +5,6 @@ import * as R from "ramda"
 
 import pseudoLocalize from "./pseudoLocalize"
 
-
 const INVALID_OBJECT_KEY_REGEX = /^(\d+[a-zA-Z]|[a-zA-Z]+\d)(\d|[a-zA-Z])*/
 export type CompiledCatalogNamespace = "cjs" | "es" | "ts" | string
 
@@ -26,11 +25,7 @@ export type CreateCompileCatalogOptions = {
  * applying pseudolocalization where necessary.
  */
 function compileSingleKey(key: string, translation: string, shouldPseudolocalize: boolean): t.ObjectProperty {
-  if (shouldPseudolocalize) {
-    translation = pseudoLocalize(key)
-  }
-
-  return t.objectProperty(t.stringLiteral(key), compile(translation))
+  return t.objectProperty(t.stringLiteral(key), compile(translation, shouldPseudolocalize))
 }
 
 export function createCompiledCatalog(
@@ -118,34 +113,40 @@ function buildExportStatement(expression, namespace: CompiledCatalogNamespace) {
  * Compile string message into AST tree. Message format is parsed/compiled into
  * JS arrays, which are handled in client.
  */
-export function compile(message: string) {
+export function compile(message: string, shouldPseudolocalize: boolean = false) {
   let tokens
 
   try {
     tokens = parse(message)
   } catch (e) {
     throw new Error(
-      `Can't parse message. Please check correct syntax: "${message}" \n \n Messageformat-parser trace: ${e.message}`,
+      `Can't parse message. Please check correct syntax: "${message}" \n \n Messageformat-parser trace: ${e.message}`
     )
   }
-  const ast = processTokens(tokens)
+  const ast = processTokens(tokens, shouldPseudolocalize)
 
   if (isString(ast)) return t.stringLiteral(ast)
 
   return ast
 }
 
-function processTokens(tokens) {
+function processTokens(tokens, shouldPseudolocalize: boolean) {
   // Shortcut - if the message doesn't include any formatting,
   // simply join all string chunks into one message
   if (!tokens.filter((token) => !isString(token)).length) {
-    return tokens.join("")
+    if (shouldPseudolocalize) {
+      return tokens.map((token) => pseudoLocalize(token)).join("")
+    } else {
+      return tokens.join("")
+    }
   }
 
   return t.arrayExpression(
     tokens.map((token) => {
       if (isString(token)) {
-        return t.stringLiteral(token)
+        return t.stringLiteral(
+          shouldPseudolocalize ? pseudoLocalize(token) : token
+        )
 
         // # in plural case
       } else if (token.type === "octothorpe") {
@@ -179,7 +180,7 @@ function processTokens(tokens) {
       }
 
       token.cases.forEach((item) => {
-        const inlineTokens = processTokens(item.tokens)
+        const inlineTokens = processTokens(item.tokens, shouldPseudolocalize)
         formatProps.push(
           t.objectProperty(
             // if starts with number must be wrapped with quotes
diff --git a/packages/cli/src/api/fixtures/pot-template/messages.pot b/packages/cli/src/api/fixtures/pot-template/messages.pot
new file mode 100644
index 00000000..d2a32f73
--- /dev/null
+++ b/packages/cli/src/api/fixtures/pot-template/messages.pot
@@ -0,0 +1,5 @@
+msgid "Hello World"
+msgstr ""
+
+msgid "Test String"
+msgstr ""
diff --git a/packages/cli/src/api/fixtures/pot-template/pl.po b/packages/cli/src/api/fixtures/pot-template/pl.po
new file mode 100644
index 00000000..67adfd62
--- /dev/null
+++ b/packages/cli/src/api/fixtures/pot-template/pl.po
@@ -0,0 +1,2 @@
+msgid "Hello World"
+msgstr "Cześć świat"
diff --git a/packages/cli/src/lingui-extract-template.ts b/packages/cli/src/lingui-extract-template.ts
index f4ebf153..4f0fd4cf 100644
--- a/packages/cli/src/lingui-extract-template.ts
+++ b/packages/cli/src/lingui-extract-template.ts
@@ -35,7 +35,7 @@ export default async function command(
 
   await Promise.all(catalogs.map(async (catalog) => {
     await catalog.makeTemplate({
-      ...options,
+      ...options as CliExtractTemplateOptions,
       orderBy: config.orderBy,
       projectType: detect(),
     })
diff --git a/packages/cli/src/lingui-extract.ts b/packages/cli/src/lingui-extract.ts
index 949ce254..6c86d63c 100644
--- a/packages/cli/src/lingui-extract.ts
+++ b/packages/cli/src/lingui-extract.ts
@@ -43,7 +43,7 @@ export default async function command(
   const catalogStats: { [path: string]: AllCatalogsType } = {}
   for (let catalog of catalogs) {
     await catalog.make({
-      ...options,
+      ...options as CliExtractOptions,
       orderBy: config.orderBy,
       extractors: config.extractors,
       projectType: detect(),
diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts
index 20ba1116..602c77d7 100644
--- a/packages/core/src/context.ts
+++ b/packages/core/src/context.ts
@@ -18,7 +18,8 @@ const defaultFormats = (
     return (ctx) => {
       const msg = isFunction(message) ? message(ctx) : message
       const norm = Array.isArray(msg) ? msg : [msg]
-      const valueStr = number(locales)(value)
+      const numberFormat = Object.keys(formats).length ? style('number') : {};
+      const valueStr = number(locales, numberFormat)(value)
       return norm.map((m) => (isString(m) ? m.replace("#", valueStr) : m))
     }
   }
diff --git a/packages/loader/package.json b/packages/loader/package.json
index 581423b5..b223f684 100644
--- a/packages/loader/package.json
+++ b/packages/loader/package.json
@@ -33,8 +33,7 @@
     "@babel/runtime": "^7.11.2",
     "@lingui/cli": "^3.14.0",
     "@lingui/conf": "^3.14.0",
-    "loader-utils": "^2.0.0",
-    "ramda": "^0.27.1"
+    "loader-utils": "^2.0.0"
   },
   "devDependencies": {
     "memory-fs": "^0.5.0"
diff --git a/packages/loader/src/webpackLoader.ts b/packages/loader/src/webpackLoader.ts
index b6cf178c..74160c79 100644
--- a/packages/loader/src/webpackLoader.ts
+++ b/packages/loader/src/webpackLoader.ts
@@ -1,5 +1,4 @@
 import path from "path"
-import * as R from "ramda"
 import { getConfig } from "@lingui/conf"
 import {
   createCompiledCatalog,
@@ -61,15 +60,11 @@ export default function (source) {
     catalogRelativePath,
     getCatalogs(config)
   )
-  const catalogs = catalog.readAll()
-  const messages = R.mapObjIndexed(
-    (_, key) =>
-      catalog.getTranslation(catalogs, locale, key, {
-        fallbackLocales: config.fallbackLocales,
-        sourceLocale: config.sourceLocale,
-      }),
-    catalogs[locale]
-  )
+
+  const messages = catalog.getTranslations(locale, {
+    fallbackLocales: config.fallbackLocales,
+    sourceLocale: config.sourceLocale,
+  })
 
   // In production we don't want untranslated strings. It's better to use message
   // keys as a last resort.
diff --git a/packages/macro/src/index.ts b/packages/macro/src/index.ts
index ed638e1b..11e7808c 100644
--- a/packages/macro/src/index.ts
+++ b/packages/macro/src/index.ts
@@ -32,6 +32,16 @@ function macro({ references, state, babel }) {
   const jsNodes = []
   let needsI18nImport = false
 
+  const alreadyVisitedCache = new WeakSet()
+  const alreadyVisited = (path) => {
+    if (alreadyVisitedCache.has(path)) {
+      return true
+    } else {
+      alreadyVisitedCache.add(path)
+      return false
+    }
+  }
+
   Object.keys(references).forEach((tagName) => {
     const nodes = references[tagName]
     const macroType = getMacroType(tagName)
@@ -121,17 +131,6 @@ function isRootPath(allPath) {
     })(node)
 }
 
-const alreadyVisitedCache = []
-
-function alreadyVisited(path) {
-  if (alreadyVisitedCache.includes(path)) {
-    return true
-  } else {
-    alreadyVisitedCache.push(path)
-    return false
-  }
-}
-
 function getMacroType(tagName) {
   switch (tagName) {
     case "defineMessage":


Test patch:
diff --git a/packages/cli/src/api/__snapshots__/catalog.test.ts.snap b/packages/cli/src/api/__snapshots__/catalog.test.ts.snap
index 26224950..7a19a761 100644
--- a/packages/cli/src/api/__snapshots__/catalog.test.ts.snap
+++ b/packages/cli/src/api/__snapshots__/catalog.test.ts.snap
@@ -1,5 +1,19 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Catalog POT Flow Should get translations from template if locale file not presented 1`] = `
+Object {
+  Hello World: Hello World,
+  Test String: Test String,
+}
+`;
+
+exports[`Catalog POT Flow Should merge source messages from template if provided 1`] = `
+Object {
+  Hello World: Cześć świat,
+  Test String: Test String,
+}
+`;
+
 exports[`Catalog collect should extract messages from source files 1`] = `
 Object {
   Component A: Object {
diff --git a/packages/cli/src/api/__snapshots__/compile.test.ts.snap b/packages/cli/src/api/__snapshots__/compile.test.ts.snap
index c16a7b14..b60c649c 100644
--- a/packages/cli/src/api/__snapshots__/compile.test.ts.snap
+++ b/packages/cli/src/api/__snapshots__/compile.test.ts.snap
@@ -6,6 +6,8 @@ Can't parse message. Please check correct syntax: "{value, plural, one {Book} ot
  Messageformat-parser trace: Expected "#", "{", "}", doubled apostrophe, escaped string, or plain char but end of input found.
 `;
 
+exports[`createCompiledCatalog nested message 1`] = `/*eslint-disable*/module.exports={messages:{"nested":{"one":"Uno","two":"Dos","three":"Tres","hello":["Hola ",["name"]]}}};`;
+
 exports[`createCompiledCatalog options.compilerBabelOptions by default should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Alohà"}};`;
 
 exports[`createCompiledCatalog options.compilerBabelOptions should return catalog without ASCII chars 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Aloh\\xE0"}};`;
@@ -20,10 +22,18 @@ exports[`createCompiledCatalog options.namespace should compile with window 1`]
 
 exports[`createCompiledCatalog options.namespace should error with invalid value 1`] = `Invalid namespace param: "global"`;
 
-exports[`createCompiledCatalog options.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ĥēĺĺō"}};`;
+exports[`createCompiledCatalog options.pseudoLocale should return catalog with pseudolocalized messages 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"ÀĥōĴ"}};`;
 
 exports[`createCompiledCatalog options.pseudoLocale should return compiled catalog when pseudoLocale doesn't match current locale 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj"}};`;
 
+exports[`createCompiledCatalog options.pure should return code catalog 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj"}};`;
+
+exports[`createCompiledCatalog options.pure should return pure catalog 1`] = `
+Object {
+  Hello: Ahoj,
+}
+`;
+
 exports[`createCompiledCatalog options.strict should return message key as a fallback translation 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj","Missing":"Missing","Select":[["id","select",{Gen:"Genesis","1John":"1 John",other:"____"}]]}};`;
 
 exports[`createCompiledCatalog options.strict should't return message key as a fallback in strict mode 1`] = `/*eslint-disable*/module.exports={messages:{"Hello":"Ahoj","Missing":"","Select":[["id","select",{Gen:"Genesis","1John":"1 John",other:"____"}]]}};`;
diff --git a/packages/cli/src/api/catalog.test.ts b/packages/cli/src/api/catalog.test.ts
index 2f58cbe4..1ad7e38f 100644
--- a/packages/cli/src/api/catalog.test.ts
+++ b/packages/cli/src/api/catalog.test.ts
@@ -131,6 +131,61 @@ describe("Catalog", function () {
     })
   })
 
+  describe("POT Flow", function () {
+    it('Should merge source messages from template if provided', () => {
+      const catalog = new Catalog(
+        {
+          name: "messages",
+          path: path.resolve(
+            __dirname,
+            path.join("fixtures", "pot-template", "{locale}")
+          ),
+          include: [],
+          exclude: [],
+        },
+        mockConfig({
+          locales: ['en', 'pl'],
+        })
+      )
+
+      const translations = catalog.getTranslations('pl', {
+        sourceLocale: 'en',
+        fallbackLocales: {
+          default: 'en'
+        }
+      });
+
+      expect(translations).toMatchSnapshot()
+    })
+
+    it('Should get translations from template if locale file not presented', () => {
+      const catalog = new Catalog(
+        {
+          name: "messages",
+          path: path.resolve(
+            __dirname,
+            path.join("fixtures", "pot-template", "{locale}")
+          ),
+          include: [],
+          exclude: [],
+        },
+        mockConfig({
+          locales: ['en', 'pl'],
+        })
+      )
+
+      const translations = catalog.getTranslations('en', {
+        sourceLocale: 'en',
+        fallbackLocales: {
+          default: 'en'
+        }
+      });
+
+      console.log(translations);
+      expect(translations).toMatchSnapshot()
+    })
+  })
+
   describe("collect", function () {
     it("should extract messages from source files", async function () {
       const catalog = new Catalog(
diff --git a/packages/cli/src/api/compile.test.ts b/packages/cli/src/api/compile.test.ts
index 7e41102a..380f28dd 100644
--- a/packages/cli/src/api/compile.test.ts
+++ b/packages/cli/src/api/compile.test.ts
@@ -2,10 +2,11 @@ import generate from "@babel/generator"
 import { compile, createCompiledCatalog } from "./compile"
 
 describe("compile", () => {
-  const getSource = (message) =>
+  const getSource = (message: string) =>
     generate(compile(message) as any, {
       compact: true,
       minified: true,
+      jsescOption: { minimal: true },
     }).code
 
   it("should optimize string only messages", () => {
@@ -67,14 +68,172 @@ describe("compile", () => {
     )
   })
 
+  it("should compile multiple plurals", () => {
+    expect(
+      getSource(
+        "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}"
+      )
+    ).toEqual(
+      '[["bcount","plural",{one:"boy",other:["#"," boys"]}]," ",["gcount","plural",{one:"girl",other:["#"," girls"]}]]'
+    )
+  })
+
   it("should report failed message on error", () => {
     expect(() =>
       getSource("{value, plural, one {Book} other {Books")
     ).toThrowErrorMatchingSnapshot()
   })
+
+  describe("with pseudo-localization", () => {
+    const getPSource = (message: string) =>
+      generate(compile(message, true) as any, {
+        compact: true,
+        minified: true,
+        jsescOption: { minimal: true },
+      }).code
+
+    it("should pseudolocalize strings", () => {
+      expect(getPSource("Martin Černý")).toEqual('"Màŕţĩń Čēŕńý"')
+    })
+
+    it("should pseudolocalize escaping syntax characters", () => {
+      // TODO: should this turn into pseudoLocale string?
+      expect(getPSource("'{name}'")).toEqual('"{name}"')
+      // expect(getPSource("'{name}'")).toEqual('"{ńàmē}"')
+    })
+
+    it("should not pseudolocalize arguments", () => {
+      expect(getPSource("{name}")).toEqual('[["name"]]')
+      expect(getPSource("B4 {name} A4")).toEqual('["ß4 ",["name"]," À4"]')
+    })
+
+    it("should not pseudolocalize arguments nor formats", () => {
+      expect(getPSource("{name, number}")).toEqual('[["name","number"]]')
+      expect(getPSource("{name, number, percent}")).toEqual(
+        '[["name","number","percent"]]'
+      )
+    })
+
+    it("should not pseudolocalize HTML tags", () => {
+      expect(getPSource('Martin <span id="spanId">Černý</span>')).toEqual(
+        JSON.stringify('Màŕţĩń <span id="spanId">Čēŕńý</span>')
+      )
+      expect(
+        getPSource("Martin Cerny  123a<span id='id'>Černý</span>")
+      ).toEqual(
+        JSON.stringify("Màŕţĩń Ćēŕńŷ  123à<span id='id'>Čēŕńý</span>")
+      )
+      expect(getPSource("Martin <a title='>>a'>a</a>")).toEqual(
+        JSON.stringify("Màŕţĩń <a title='>>a'>à</a>")
+      )
+      expect(getPSource("<a title=TITLE>text</a>")).toEqual(
+        JSON.stringify("<a title=TITLE>ţēxţ</a>")
+      )
+    })
+
+    describe("Plurals", () => {
+      it("with value", () => {
+        expect(
+          getPSource("{value, plural, one {# book} other {# books}}")
+        ).toEqual('[["value","plural",{one:["#"," ƀōōķ"],other:["#"," ƀōōķś"]}]]')
+      })
+
+      it("with variable placeholder", () => {
+        expect(
+          getPSource(
+            "{count, plural, one {{countString} book} other {{countString} books}}"
+          )
+        ).toEqual(
+          '[["count","plural",{one:[["countString"]," ƀōōķ"],other:[["countString"]," ƀōōķś"]}]]'
+        )
+      })
+
+      it("with offset", () => {
+        expect(
+          getPSource(
+            "{count, plural, offset:1 zero {There are no messages} other {There are # messages in your inbox}}"
+          )
+        ).toEqual(
+          '[["count","plural",{offset:1,zero:"Ţĥēŕē àŕē ńō mēśśàĝēś",other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś ĩń ŷōũŕ ĩńƀōx"]}]]'
+        )
+      })
+
+      it("with HTML tags", () => {
+        expect(
+          getPSource(
+            "{count, plural, zero {There's # <span>message</span>} other {There are # messages}}"
+          )
+        ).toEqual(
+          '[["count","plural",{zero:["Ţĥēŕē\'ś ","#"," <span>mēśśàĝē</span>"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]'
+        )
+      })
+
+      it("with exact number", () => {
+        expect(
+          getPSource(
+            "{count, plural, =0 {There's # <span>message</span>} other {There are # messages}}"
+          )
+        ).toEqual(
+          '[["count","plural",{0:["Ţĥēŕē\'ś ","#"," <span>mēśśàĝē</span>"],other:["Ţĥēŕē àŕē ","#"," mēśśàĝēś"]}]]'
+        )
+      })
+    })
+
+    it("SelectOrdinal", () => {
+      expect(
+        getPSource(
+          "{count, selectordinal, offset:1 one {#st} two {#nd} few {#rd} =4 {4th} many {testMany} other {#th}}"
+        )
+      ).toEqual(
+          '[["count","selectordinal",{offset:1,one:["#","śţ"],two:["#","ńď"],few:["#","ŕď"],4:"4ţĥ",many:"ţēśţMàńŷ",other:["#","ţĥ"]}]]'
+      )
+    })
+
+    it("Select", () => {
+      expect(
+        getPSource(
+          "{gender, select, male {He} female {She} other {<span>Other</span>}}"
+        )
+      ).toEqual(
+        '[["gender","select",{male:"Ĥē",female:"Śĥē",other:"<span>Ōţĥēŕ</span>"}]]'
+      )
+    })
+
+    it("should not pseudolocalize variables", () => {
+      expect(getPSource("replace {count}")).toEqual('["ŕēƥĺàćē ",["count"]]')
+      expect(getPSource("replace { count }")).toEqual('["ŕēƥĺàćē ",["count"]]')
+    })
+
+    it("Multiple Plurals", () => {
+      expect(
+        getPSource(
+          "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}"
+        )
+      ).toEqual(
+        '[["bcount","plural",{one:"ƀōŷ",other:["#"," ƀōŷś"]}]," ",["gcount","plural",{one:"ĝĩŕĺ",other:["#"," ĝĩŕĺś"]}]]'
+      )
+    })
+  })
 })
 
 describe("createCompiledCatalog", () => {
+  it("nested message", () => {
+      expect(
+        createCompiledCatalog(
+          "cs",
+          {
+            nested: {
+              one: "Uno",
+              two: "Dos",
+              three: "Tres",
+              hello: "Hola {name}",
+            },
+          },
+          {}
+        )
+      ).toMatchSnapshot()
+  })
+
   describe("options.namespace", () => {
     const getCompiledCatalog = (namespace) =>
       createCompiledCatalog(
@@ -113,7 +272,7 @@ describe("createCompiledCatalog", () => {
         {
           Hello: "Ahoj",
           Missing: "",
-          Select: "{id, select, Gen {Genesis} 1John {1 John}  other {____}}"
+          Select: "{id, select, Gen {Genesis} 1John {1 John}  other {____}}",
         },
         {
           strict,
@@ -150,6 +309,27 @@ describe("createCompiledCatalog", () => {
     })
   })
 
+  describe("options.pure", () => {
+    const getCompiledCatalog = (pure) =>
+      createCompiledCatalog(
+        "ps",
+        {
+          Hello: "Ahoj",
+        },
+        {
+          pure,
+        }
+      )
+
+    it("should return pure catalog", () => {
+      expect(getCompiledCatalog(true)).toMatchSnapshot()
+    })
+
+    it("should return code catalog", () => {
+      expect(getCompiledCatalog(false)).toMatchSnapshot()
+    })
+  })
+
   describe("options.compilerBabelOptions", () => {
     const getCompiledCatalog = (opts = {}) =>
       createCompiledCatalog(
@@ -165,13 +345,15 @@ describe("createCompiledCatalog", () => {
     })
 
     it("should return catalog without ASCII chars", () => {
-      expect(getCompiledCatalog({
-        compilerBabelOptions: { 
-          jsescOption: {
-            minimal: false,
-          }
-        }
-      })).toMatchSnapshot()
+      expect(
+        getCompiledCatalog({
+          compilerBabelOptions: {
+            jsescOption: {
+              minimal: false,
+            },
+          },
+        })
+      ).toMatchSnapshot()
     })
   })
 })
diff --git a/packages/cli/src/api/pseudoLocalize.test.ts b/packages/cli/src/api/pseudoLocalize.test.ts
index 4dd0b187..1b9c7d20 100644
--- a/packages/cli/src/api/pseudoLocalize.test.ts
+++ b/packages/cli/src/api/pseudoLocalize.test.ts
@@ -50,20 +50,20 @@ describe("PseudoLocalization", () => {
     it("with HTML tags", () => {
       expect(
         pseudoLocalize(
-          "{count, plural, zero {There's # <span>message</span>} other {There are # messages}"
+          "{count, plural, zero {There's # <span>message</span>} other {There are # messages}}"
         )
       ).toEqual(
-        "{count, plural, zero {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē àŕē # mēśśàĝēś}"
+        "{count, plural, zero {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē àŕē # mēśśàĝēś}}"
       )
     })
 
     it("with exact number", () => {
       expect(
         pseudoLocalize(
-          "{count, plural, =0 {There's # <span>message</span>} other {There are # messages}"
+          "{count, plural, =0 {There's # <span>message</span>} other {There are # messages}}"
         )
       ).toEqual(
-        "{count, plural, =0 {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē àŕē # mēśśàĝēś}"
+        "{count, plural, =0 {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē àŕē # mēśśàĝēś}}"
       )
     })
   })
@@ -92,4 +92,14 @@ describe("PseudoLocalization", () => {
     expect(pseudoLocalize("replace {count}")).toEqual("ŕēƥĺàćē {count}")
     expect(pseudoLocalize("replace { count }")).toEqual("ŕēƥĺàćē { count }")
   })
+
+  it("multiple plurals pseudolocalize gives wrong ICU message", () => {
+    expect(
+      pseudoLocalize(
+        "{bcount, plural, one {boy} other {# boys}} {gcount, plural, one {girl} other {# girls}}"
+      )
+    ).not.toEqual(
+      "{bcount, plural, one {ƀōŷ} other {# ƀōŷś}} {gcount, plural, one {ĝĩŕĺ} other {# ĝĩŕĺś}}"
+    )
+  })
 })