Repository: lingui/js-lingui (ts)

Issue:
Add config option for formatOptions - origins - lineNumbers: true / false
The issue is described in https://github.com/lingui/js-lingui/issues/587

The current state is there is an option in Lingui configuration "origins", which enables / disables including file paths -and- line numbers of origin in the catalog. Origins provide some context information for the translator but at the same time, especially line numbers change regularly. The file part of the origin doesn't change that much and is the part that gives context to translation. My proposal is - if there is some use for keeping line numbers in origins for someone, to add an option to 'origins' - something like 'lineNumbers: true/false" (possibly making false the default). If there isn't really need to keep the line numbers in 'origins', to remove them. What do you think?


Pull request description:
feat: allow to disable lineNumbers
Will close #805

Patch:
diff --git a/docs/ref/conf.rst b/docs/ref/conf.rst
index 3dbc4409..9403d981 100644
--- a/docs/ref/conf.rst
+++ b/docs/ref/conf.rst
@@ -388,12 +388,14 @@ Origin is filename and line number from where the message was extracted.
 Note that origins may produce a large amount of merge conflicts. Origins can be
 disabled by setting ``origins: false`` in :conf:`formatOptions`.
 
+Also, you can disable just ``lineNumbers`` but keep ``origins``
+
 .. config:: formatOptions
 
 formatOptions
 -------------
 
-Default: ``{ origins: true }``
+Default: ``{ origins: true, lineNumbers: true }``
 
 Object for configuring message catalog output. See individual formats for options.
 
diff --git a/docs/tutorials/javascript.rst b/docs/tutorials/javascript.rst
index d5352fa9..e063fad5 100644
--- a/docs/tutorials/javascript.rst
+++ b/docs/tutorials/javascript.rst
@@ -97,8 +97,7 @@ Plurals and selections are possible using plural and select methods:
 
    const count = 42
 
-   plural({
-     value: count,
+   plural(count, {
      one: "# book",
      other: "# books"
    })
diff --git a/packages/cli/src/api/catalog.ts b/packages/cli/src/api/catalog.ts
index 91084c56..7bd24973 100644
--- a/packages/cli/src/api/catalog.ts
+++ b/packages/cli/src/api/catalog.ts
@@ -36,7 +36,7 @@ export type MessageType = ExtractedMessageType & {
   translation: string
 }
 
-type ExtractedCatalogType = {
+export type ExtractedCatalogType = {
   [msgId: string]: ExtractedMessageType
 }
 
diff --git a/packages/cli/src/api/formats/lingui.ts b/packages/cli/src/api/formats/lingui.ts
index 357d6c89..c06c70a7 100644
--- a/packages/cli/src/api/formats/lingui.ts
+++ b/packages/cli/src/api/formats/lingui.ts
@@ -2,7 +2,7 @@ import fs from "fs"
 import * as R from "ramda"
 
 import { writeFileIfChanged } from "../utils"
-import { CatalogType } from "../catalog"
+import { ExtractedMessageType, CatalogType } from "../catalog"
 import { CatalogFormatter } from "."
 
 type NoOriginsCatalogType = {
@@ -13,6 +13,16 @@ const removeOrigins = (R.map(
   ({ origin, ...message }) => message
 ) as unknown) as (catalog: CatalogType) => NoOriginsCatalogType
 
+const removeLineNumbers = (R.map(
+  (message: ExtractedMessageType) => {
+    if (message.origin) {
+      message.origin.map(originValue => originValue.pop())
+    }
+    return message
+  }
+) as unknown) as (catalog: ExtractedMessageType) => NoOriginsCatalogType
+
+
 const lingui: CatalogFormatter = {
   catalogExtension: ".json",
 
@@ -21,6 +31,9 @@ const lingui: CatalogFormatter = {
     if (options.origins === false) {
       outputCatalog = removeOrigins(catalog)
     }
+    if (options.origins !== false && options.lineNumbers === false) {
+      outputCatalog = removeLineNumbers(outputCatalog)
+    }
     writeFileIfChanged(filename, JSON.stringify(outputCatalog, null, 2))
   },
 
diff --git a/packages/cli/src/api/formats/po.ts b/packages/cli/src/api/formats/po.ts
index bf0895e7..33f8e118 100644
--- a/packages/cli/src/api/formats/po.ts
+++ b/packages/cli/src/api/formats/po.ts
@@ -25,8 +25,15 @@ const serialize = (items: CatalogType, options) =>
       item.msgstr = [message.translation]
       item.comments = message.comments || []
       item.extractedComments = message.extractedComments || []
-      if (options.origins) {
-        item.references = message.origin ? message.origin.map(joinOrigin) : []
+      if (options.origins !== false) {
+        if (message.origin && options.lineNumbers === false) {
+          item.references = message.origin.map(msg => {
+            msg.pop()
+            return msg
+          }).map(joinOrigin)
+        } else {
+          item.references = message.origin ? message.origin.map(joinOrigin) : []
+        }
       }
       // @ts-ignore: Figure out how to set this flag
       item.obsolete = message.obsolete
diff --git a/packages/conf/index.d.ts b/packages/conf/index.d.ts
index 3d8b266d..408c1520 100644
--- a/packages/conf/index.d.ts
+++ b/packages/conf/index.d.ts
@@ -3,6 +3,7 @@ import type { GeneratorOptions } from "@babel/core";
 export declare type CatalogFormat = "lingui" | "minimal" | "po" | "csv" | "po-gettext";
 export type CatalogFormatOptions = {
     origins?: boolean;
+    lineNumbers?: boolean;
 }
 export declare type OrderBy = "messageId" | "origin";
 declare type CatalogConfig = {
diff --git a/packages/conf/src/index.ts b/packages/conf/src/index.ts
index 822d2618..7eda2c13 100644
--- a/packages/conf/src/index.ts
+++ b/packages/conf/src/index.ts
@@ -10,6 +10,7 @@ export type CatalogFormat = "lingui" | "minimal" | "po" | "csv"
 
 export type CatalogFormatOptions = {
   origins?: boolean
+  lineNumbers?: boolean
 }
 
 export type OrderBy = "messageId" | "origin"
@@ -76,7 +77,7 @@ export const defaultConfig: LinguiConfig = {
   extractBabelOptions: { plugins: [], presets: [] },
   fallbackLocales: {},
   format: "po",
-  formatOptions: { origins: true },
+  formatOptions: { origins: true, lineNumbers: true },
   locales: [],
   orderBy: "messageId",
   pseudoLocale: "",


Test patch:
diff --git a/packages/cli/src/api/formats/lingui.test.ts b/packages/cli/src/api/formats/lingui.test.ts
index c93efcb5..5a5bf1db 100644
--- a/packages/cli/src/api/formats/lingui.test.ts
+++ b/packages/cli/src/api/formats/lingui.test.ts
@@ -38,7 +38,9 @@ describe("lingui format", function () {
       },
       withDescription: {
         translation: "Message with description",
-        extractedComments: ["Description is comment from developers to translators"],
+        extractedComments: [
+          "Description is comment from developers to translators",
+        ],
       },
       withComments: {
         comments: ["Translator comment", "This one might come from developer"],
@@ -122,4 +124,59 @@ describe("lingui format", function () {
     const linguiOriginProperty = '"origin"'
     expect(lingui).toEqual(expect.not.stringContaining(linguiOriginProperty))
   })
+
+  it("should not include lineNumbers if lineNumbers option is false", function () {
+    mockFs({
+      locale: {
+        en: mockFs.directory(),
+      },
+    })
+
+    const filename = path.join("locale", "en", "messages.json")
+    const catalog: CatalogType = {
+      static: {
+        translation: "Static message",
+      },
+      withOrigin: {
+        translation: "Message with origin",
+        origin: [["src/App.js", 4]],
+      },
+      withMultipleOrigins: {
+        translation: "Message with multiple origin",
+        origin: [
+          ["src/App.js", 4],
+          ["src/Component.js", 2],
+        ],
+      },
+    }
+    format.write(filename, catalog, { lineNumbers: false, locale: "en" })
+    const lingui = fs.readFileSync(filename).toString()
+    mockFs.restore()
+    expect(lingui).toMatchInlineSnapshot(`
+      {
+        "static": {
+          "translation": "Static message"
+        },
+        "withOrigin": {
+          "translation": "Message with origin",
+          "origin": [
+            [
+              "src/App.js"
+            ]
+          ]
+        },
+        "withMultipleOrigins": {
+          "translation": "Message with multiple origin",
+          "origin": [
+            [
+              "src/App.js"
+            ],
+            [
+              "src/Component.js"
+            ]
+          ]
+        }
+      }
+    `)
+  })
 })
diff --git a/packages/cli/src/api/formats/po.test.ts b/packages/cli/src/api/formats/po.test.ts
index 3cb2c5de..2ff33a1a 100644
--- a/packages/cli/src/api/formats/po.test.ts
+++ b/packages/cli/src/api/formats/po.test.ts
@@ -4,23 +4,24 @@ import mockFs from "mock-fs"
 import mockDate from "mockdate"
 import PO from "pofile"
 import { mockConsole } from "@lingui/jest-mocks"
+import { format as formatDate } from "date-fns"
 
 import format from "./po"
 import { CatalogType } from "../catalog"
 
-describe("pofile format", function () {
+describe("pofile format", () => {
   afterEach(() => {
     mockFs.restore()
     mockDate.reset()
   })
 
-  it("should write catalog in pofile format", function () {
+  it("should write catalog in pofile format", () => {
     mockFs({
       locale: {
         en: mockFs.directory(),
       },
     })
-    mockDate.set(new Date(2018,7,27,10,0,0).toUTCString())
+    mockDate.set(new Date(2018, 7, 27, 10, 0, 0).toUTCString())
 
     const filename = path.join("locale", "en", "messages.po")
     const catalog: CatalogType = {
@@ -40,7 +41,9 @@ describe("pofile format", function () {
       },
       withDescription: {
         translation: "Message with description",
-        extractedComments: ["Description is comment from developers to translators"],
+        extractedComments: [
+          "Description is comment from developers to translators",
+        ],
       },
       withComments: {
         comments: ["Translator comment", "This one might come from developer"],
@@ -73,7 +76,7 @@ describe("pofile format", function () {
     expect(pofile).toMatchSnapshot()
   })
 
-  it("should read catalog in pofile format", function () {
+  it("should read catalog in pofile format", () => {
     const pofile = fs
       .readFileSync(
         path.join(path.resolve(__dirname), "fixtures", "messages.po")
@@ -94,7 +97,7 @@ describe("pofile format", function () {
     expect(actual).toMatchSnapshot()
   })
 
-  it("should correct badly used comments", function () {
+  it("should correct badly used comments", () => {
     const po = PO.parse(`
       #. First description
       #. Second comment
@@ -123,7 +126,7 @@ describe("pofile format", function () {
     expect(actual).toMatchSnapshot()
   })
 
-  it("should throw away additional msgstr if present", function () {
+  it("should throw away additional msgstr if present", () => {
     const po = PO.parse(`
       msgid "withMultipleTranslation"
       msgstr[0] "This is just fine"
@@ -150,7 +153,7 @@ describe("pofile format", function () {
     })
   })
 
-  it("should write the same catalog as it was read", function () {
+  it("should write the same catalog as it was read", () => {
     const pofile = fs
       .readFileSync(
         path.join(path.resolve(__dirname), "fixtures", "messages.po")
@@ -172,10 +175,12 @@ describe("pofile format", function () {
     mockFs.restore()
     // on windows mockFs adds ··· to multiline string, so this strictly equal comparison can't be done
     // we test that the content if the same inlined...
-    expect(actual.replace(/(\r\n|\n|\r)/gm,"")).toEqual(pofile.replace(/(\r\n|\n|\r)/gm,""))
+    expect(actual.replace(/(\r\n|\n|\r)/gm, "")).toEqual(
+      pofile.replace(/(\r\n|\n|\r)/gm, "")
+    )
   })
 
-  it("should not include origins if origins option is false", function () {
+  it("should not include origins if origins option is false", () => {
     mockFs({
       locale: {
         en: mockFs.directory(),
@@ -205,4 +210,60 @@ describe("pofile format", function () {
     const pofileOriginPrefix = "#:"
     expect(pofile).toEqual(expect.not.stringContaining(pofileOriginPrefix))
   })
+
+  it("should not include lineNumbers if lineNumbers option is false", () => {
+    mockFs({
+      locale: {
+        en: mockFs.directory(),
+      },
+    })
+
+    const filename = path.join("locale", "en", "messages.po")
+    const catalog: CatalogType = {
+      static: {
+        translation: "Static message",
+      },
+      withOrigin: {
+        translation: "Message with origin",
+        origin: [["src/App.js", 4]],
+      },
+      withMultipleOrigins: {
+        translation: "Message with multiple origin",
+        origin: [
+          ["src/App.js", 4],
+          ["src/Component.js", 2],
+        ],
+      },
+    }
+    format.write(filename, catalog, {
+      origins: true,
+      lineNumbers: false,
+      locale: "en",
+    })
+    const pofile = fs.readFileSync(filename).toString()
+    mockFs.restore()
+    expect(pofile).toMatchInlineSnapshot(`
+      msgid ""
+      msgstr ""
+      "POT-Creation-Date: ${formatDate(new Date(), "yyyy-MM-dd HH:mmxxxx")}\\n"
+      "Mime-Version: 1.0\\n"
+      "Content-Type: text/plain; charset=utf-8\\n"
+      "Content-Transfer-Encoding: 8bit\\n"
+      "X-Generator: @lingui/cli\\n"
+      "Language: en\\n"
+
+      msgid "static"
+      msgstr "Static message"
+
+      #: src/App.js
+      msgid "withOrigin"
+      msgstr "Message with origin"
+
+      #: src/App.js
+      #: src/Component.js
+      msgid "withMultipleOrigins"
+      msgstr "Message with multiple origin"
+
+    `)
+  })
 })
diff --git a/packages/conf/src/__snapshots__/index.test.ts.snap b/packages/conf/src/__snapshots__/index.test.ts.snap
index 56fcc524..3f88372b 100644
--- a/packages/conf/src/__snapshots__/index.test.ts.snap
+++ b/packages/conf/src/__snapshots__/index.test.ts.snap
@@ -125,6 +125,7 @@ Object {
   },
   format: po,
   formatOptions: Object {
+    lineNumbers: true,
     origins: true,
   },
   locales: Array [