Repository: lingui/js-lingui (ts)

Issue:
Lingui Extract Erases All Messages when JSON is Invalid
**Describe the bug**

Lingui overwrites messages.json if the messages.json is invalid on lingui extract.

**To Reproduce**

Run `lingui extract` with minimal output. Fill in values for one of the locales, and then make the json invalid (e.g.. drop a comma). Run `lingui extract` again. All of the messages will now be blank.

**Expected behavior**

Lingui should crash with an error that says the messages file is invalid json.


Pull request description:
fix: formatters exceptions throw error
Will close #888 

Patch:
diff --git a/packages/cli/src/api/formats/csv.ts b/packages/cli/src/api/formats/csv.ts
index 0a65caa2..73f8901e 100644
--- a/packages/cli/src/api/formats/csv.ts
+++ b/packages/cli/src/api/formats/csv.ts
@@ -45,8 +45,7 @@ const csv: CatalogFormatter = {
     try {
       return deserialize(raw)
     } catch (e) {
-      console.error(`Cannot read ${filename}: ${e.message}`)
-      return null
+      throw new Error(`Cannot read ${filename}: ${e.message}`)
     }
   },
 }
diff --git a/packages/cli/src/api/formats/lingui.ts b/packages/cli/src/api/formats/lingui.ts
index b14dc133..357d6c89 100644
--- a/packages/cli/src/api/formats/lingui.ts
+++ b/packages/cli/src/api/formats/lingui.ts
@@ -30,8 +30,7 @@ const lingui: CatalogFormatter = {
     try {
       return JSON.parse(raw)
     } catch (e) {
-      console.error(`Cannot read ${filename}: ${e.message}`)
-      return null
+      throw new Error(`Cannot read ${filename}: ${e.message}`)
     }
   },
 }
diff --git a/packages/cli/src/api/formats/minimal.ts b/packages/cli/src/api/formats/minimal.ts
index b5e04cf9..e59cc0cf 100644
--- a/packages/cli/src/api/formats/minimal.ts
+++ b/packages/cli/src/api/formats/minimal.ts
@@ -33,8 +33,7 @@ const minimal: CatalogFormatter = {
       const rawCatalog: Record<string, string> = JSON.parse(raw)
       return deserialize(rawCatalog)
     } catch (e) {
-      console.error(`Cannot read ${filename}: ${e.message}`)
-      return null
+      throw new Error(`Cannot read ${filename}: ${e.message}`)
     }
   },
 }
diff --git a/packages/cli/src/api/formats/po.ts b/packages/cli/src/api/formats/po.ts
index bcc18e94..bf0895e7 100644
--- a/packages/cli/src/api/formats/po.ts
+++ b/packages/cli/src/api/formats/po.ts
@@ -62,6 +62,9 @@ const deserialize: (item: Object) => Object = R.map(
 )
 
 type POItem = InstanceType<typeof PO.Item>
+type PoFormatter = {
+  parse: (raw: string) => Object
+}
 
 const validateItems = R.forEach<POItem>((item) => {
   if (R.length(getTranslations(item)) > 1) {
@@ -74,7 +77,7 @@ const validateItems = R.forEach<POItem>((item) => {
 
 const indexItems = R.indexBy(getMessageKey)
 
-const po: CatalogFormatter = {
+const po: CatalogFormatter & PoFormatter = {
   catalogExtension: ".po",
 
   write(filename, catalog, options) {
@@ -99,7 +102,6 @@ const po: CatalogFormatter = {
     return this.parse(raw)
   },
 
-  // @ts-ignore
   parse(raw) {
     const po = PO.parse(raw)
     validateItems(po.items)
diff --git a/packages/cli/src/api/pseudoLocalize.ts b/packages/cli/src/api/pseudoLocalize.ts
index ab904cc7..098bc429 100644
--- a/packages/cli/src/api/pseudoLocalize.ts
+++ b/packages/cli/src/api/pseudoLocalize.ts
@@ -16,9 +16,9 @@ Example: https://regex101.com/r/bDHD9z/3
 const HTMLRegex = /<\/?\w+((\s+\w+(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)\/?>/g
 /*
 Regex should match js-lingui plurals
-Example: https://regex101.com/r/utnbQw/1
+Example: https://regex101.com/r/utnbQw/3
 */
-const PluralRegex = /{\w*,\s*plural,\s*\w*\s*{|}\s*(zero|one|two|few|many|other)\s*({|})/g
+const PluralRegex = /({\w*,\s*plural,(.|\n)*?{)|(}\s*(offset|zero|one|two|few|many|other)\s*{)/g
 /*
 Regex should match js-lingui variables
 Example: https://regex101.com/r/dw1QHb/2
diff --git a/packages/macro/index.d.ts b/packages/macro/index.d.ts
index 33f64fa4..cb3a6172 100644
--- a/packages/macro/index.d.ts
+++ b/packages/macro/index.d.ts
@@ -40,10 +40,26 @@ export type ChoiceProps = {
 } & TransProps &
   ChoiceOptions<ReactNode>
 
+/**
+ * The types should be changed after this PR is merged
+ * https://github.com/Microsoft/TypeScript/pull/26797
+ *
+ * then we should be able to specify that key of values is same type as value.
+ * We would be able to remove separate type Values = {...} definition
+ * eg.
+ * type SelectProps<Values> = {
+ *  value?: Values
+ *  [key: Values]: string
+ * }
+ *
+ */
+type Values = { [key: string]: string }
+
 export type SelectProps = {
-  value?: string
-  other?: ReactNode
-} & TransProps
+  value: string
+  other: ReactNode
+} & TransProps &
+  Values
 
 export const Trans: ComponentType<TransProps>
 export const Plural: ComponentType<ChoiceProps>


Test patch:
diff --git a/packages/cli/src/api/pseudoLocalize.test.ts b/packages/cli/src/api/pseudoLocalize.test.ts
index e354e919..6108606f 100644
--- a/packages/cli/src/api/pseudoLocalize.test.ts
+++ b/packages/cli/src/api/pseudoLocalize.test.ts
@@ -20,30 +20,55 @@ describe("PseudoLocalization", () => {
     )
   })
 
-  it("should pseudlocalize plurals with HTML tags", () => {
-    expect(
-      pseudoLocalize(
-        "{messagesCount, plural, zero {There's # <span>message</span>} other {There're # messages}"
+  describe("Plurals", () => {
+    it("with value", () => {
+      expect(
+        pseudoLocalize("{value, plural, one {# book} other {# books}}")
+      ).toEqual("{value, plural, one {# ƀōōķ} other {# ƀōōķś}}")
+    })
+
+    it("with variable placeholder", () => {
+      expect(
+        pseudoLocalize(
+          "{count, plural, one {{countString} book} other {{countString} books}}"
+        )
+      ).toEqual(
+        "{count, plural, one {{countString} ƀōōķ} other {{countString} ƀōōķś}}"
       )
-    ).toEqual(
-      "{messagesCount, plural, zero {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē'ŕē # mēśśàĝēś}"
-    )
-  })
+    })
 
-  it("should pseudolocalize plurals", () => {
-    expect(
-      pseudoLocalize("{value, plural, one {# book} other {# books}}")
-    ).toEqual("{value, plural, one {# ƀōōķ} other {# ƀōōķś}}")
-    expect(
-      pseudoLocalize(
-        "{count, plural, one {{countString} book} other {{countString} books}}"
+    it("with offset", () => {
+      expect(
+        pseudoLocalize(
+          "{count, plural, offset:1 zero {There're no messages} other {There're # messages in your inbox}}"
+        )
+      ).toEqual(
+        "{count, plural, offset:1 zero {Ţĥēŕē'ŕē ńō mēśśàĝēś} other {Ţĥēŕē'ŕē # mēśśàĝēś ĩń ŷōũŕ ĩńƀōx}}"
       )
-    ).toEqual(
-      "{count, plural, one {{countString} ƀōōķ} other {{countString} ƀōōķś}}"
-    )
+    })
+
+    it("with HTML tags", () => {
+      expect(
+        pseudoLocalize(
+          "{count, plural, zero {There's # <span>message</span>} other {There're # messages}"
+        )
+      ).toEqual(
+        "{count, plural, zero {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē'ŕē # mēśśàĝēś}"
+      )
+    })
+
+    it("with exact number", () => {
+      expect(
+        pseudoLocalize(
+          "{count, plural, =0 {There's # <span>message</span>} other {There're # messages}"
+        )
+      ).toEqual(
+        "{count, plural, =0 {Ţĥēŕē'ś # <span>mēśśàĝē</span>} other {Ţĥēŕē'ŕē # mēśśàĝēś}"
+      )
+    })
   })
 
-  it("shouldn't pseudolocalize variables", () => {
+  it("should not pseudolocalize variables", () => {
     expect(pseudoLocalize("replace {count}")).toEqual("ŕēƥĺàćē {count}")
     expect(pseudoLocalize("replace { count }")).toEqual("ŕēƥĺàćē { count }")
   })