Skip to content

Commit 3303228

Browse files
feat(po-format): add explicitIdAsDefault for po-format for easier migration (#1672)
* feat(po-format): add `explicitIdAsDefault` for po-format for easier migration * docs: add lazy translations migrations notes
1 parent f06cdf5 commit 3303228

File tree

6 files changed

+323
-36
lines changed

6 files changed

+323
-36
lines changed

packages/format-po/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,21 @@ export type PoFormatterOptions = {
6161
* @default false
6262
*/
6363
printLinguiId?: boolean
64+
65+
/**
66+
* By default, the po-formatter treats the pair `msgid` + `msgctx` as the source
67+
* for generating an ID by hashing its value.
68+
*
69+
* For messages with explicit IDs, the formatter adds a special comment `js-lingui-explicit-id` as a flag.
70+
* When this flag is present, the formatter will use the `msgid` as-is without any additional processing.
71+
*
72+
* Set this option to true if you exclusively use explicit-ids in your project.
73+
*
74+
* https://lingui.dev/tutorials/react-patterns#using-custom-id
75+
*
76+
* @default false
77+
*/
78+
explicitIdAsDefault?: boolean
6479
}
6580
```
6681

packages/format-po/src/po.test.ts

+142
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,148 @@ describe("pofile format", () => {
122122
expect(actual).toMatchObject(catalog)
123123
})
124124

125+
describe("explicitIdAsDefault", () => {
126+
const catalog: CatalogType = {
127+
// with generated id
128+
Dgzql1: {
129+
message: "with generated id",
130+
translation: "",
131+
context: "my context",
132+
},
133+
134+
"custom.id": {
135+
message: "with explicit id",
136+
translation: "",
137+
},
138+
}
139+
140+
it("should set `js-lingui-generated-id` for messages with generated id when [explicitIdAsDefault: true]", () => {
141+
const format = createFormatter({
142+
origins: true,
143+
explicitIdAsDefault: true,
144+
})
145+
146+
const serialized = format.serialize(
147+
catalog,
148+
defaultSerializeCtx
149+
) as string
150+
151+
expect(serialized).toMatchInlineSnapshot(`
152+
msgid ""
153+
msgstr ""
154+
"POT-Creation-Date: 2018-08-27 10:00+0000\\n"
155+
"MIME-Version: 1.0\\n"
156+
"Content-Type: text/plain; charset=utf-8\\n"
157+
"Content-Transfer-Encoding: 8bit\\n"
158+
"X-Generator: @lingui/cli\\n"
159+
"Language: en\\n"
160+
161+
#. js-lingui-generated-id
162+
msgctxt "my context"
163+
msgid "with generated id"
164+
msgstr ""
165+
166+
msgid "custom.id"
167+
msgstr ""
168+
169+
`)
170+
171+
const actual = format.parse(serialized, defaultParseCtx)
172+
expect(actual).toMatchInlineSnapshot(`
173+
{
174+
Dgzql1: {
175+
comments: [
176+
js-lingui-generated-id,
177+
],
178+
context: my context,
179+
extra: {
180+
flags: [],
181+
translatorComments: [],
182+
},
183+
message: with generated id,
184+
obsolete: false,
185+
origin: [],
186+
translation: ,
187+
},
188+
custom.id: {
189+
comments: [],
190+
context: null,
191+
extra: {
192+
flags: [],
193+
translatorComments: [],
194+
},
195+
obsolete: false,
196+
origin: [],
197+
translation: ,
198+
},
199+
}
200+
`)
201+
})
202+
203+
it("should set `js-explicit-id` for messages with explicit id when [explicitIdAsDefault: false]", () => {
204+
const format = createFormatter({
205+
origins: true,
206+
explicitIdAsDefault: false,
207+
})
208+
209+
const serialized = format.serialize(
210+
catalog,
211+
defaultSerializeCtx
212+
) as string
213+
214+
expect(serialized).toMatchInlineSnapshot(`
215+
msgid ""
216+
msgstr ""
217+
"POT-Creation-Date: 2018-08-27 10:00+0000\\n"
218+
"MIME-Version: 1.0\\n"
219+
"Content-Type: text/plain; charset=utf-8\\n"
220+
"Content-Transfer-Encoding: 8bit\\n"
221+
"X-Generator: @lingui/cli\\n"
222+
"Language: en\\n"
223+
224+
msgctxt "my context"
225+
msgid "with generated id"
226+
msgstr ""
227+
228+
#. js-lingui-explicit-id
229+
msgid "custom.id"
230+
msgstr ""
231+
232+
`)
233+
234+
const actual = format.parse(serialized, defaultParseCtx)
235+
expect(actual).toMatchInlineSnapshot(`
236+
{
237+
Dgzql1: {
238+
comments: [],
239+
context: my context,
240+
extra: {
241+
flags: [],
242+
translatorComments: [],
243+
},
244+
message: with generated id,
245+
obsolete: false,
246+
origin: [],
247+
translation: ,
248+
},
249+
custom.id: {
250+
comments: [
251+
js-lingui-explicit-id,
252+
],
253+
context: null,
254+
extra: {
255+
flags: [],
256+
translatorComments: [],
257+
},
258+
obsolete: false,
259+
origin: [],
260+
translation: ,
261+
},
262+
}
263+
`)
264+
})
265+
})
266+
125267
it("should print lingui id if printLinguiId = true", () => {
126268
const format = createFormatter({ origins: true, printLinguiId: true })
127269

packages/format-po/src/po.ts

+37-5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,21 @@ export type PoFormatterOptions = {
4343
* @default false
4444
*/
4545
printLinguiId?: boolean
46+
47+
/**
48+
* By default, the po-formatter treats the pair `msgid` + `msgctx` as the source
49+
* for generating an ID by hashing its value.
50+
*
51+
* For messages with explicit IDs, the formatter adds a special comment `js-lingui-explicit-id` as a flag.
52+
* When this flag is present, the formatter will use the `msgid` as-is without any additional processing.
53+
*
54+
* Set this option to true if you exclusively use explicit-ids in your project.
55+
*
56+
* https://lingui.dev/tutorials/react-patterns#using-custom-id
57+
*
58+
* @default false
59+
*/
60+
explicitIdAsDefault?: boolean
4661
}
4762

4863
function isGeneratedId(id: string, message: MessageType): boolean {
@@ -61,6 +76,7 @@ function getCreateHeaders(language: string): PO["headers"] {
6176
}
6277

6378
const EXPLICIT_ID_FLAG = "js-lingui-explicit-id"
79+
const GENERATED_ID_FLAG = "js-lingui-generated-id"
6480

6581
const serialize = (catalog: CatalogType, options: PoFormatterOptions) => {
6682
return Object.keys(catalog).map((id) => {
@@ -84,15 +100,24 @@ const serialize = (catalog: CatalogType, options: PoFormatterOptions) => {
84100
if (_isGeneratedId) {
85101
item.msgid = message.message
86102

103+
if (options.explicitIdAsDefault) {
104+
if (!item.extractedComments.includes(GENERATED_ID_FLAG)) {
105+
item.extractedComments.push(GENERATED_ID_FLAG)
106+
}
107+
}
108+
87109
if (options.printLinguiId) {
88110
if (!item.extractedComments.find((c) => c.includes("js-lingui-id"))) {
89111
item.extractedComments.push(`js-lingui-id: ${id}`)
90112
}
91113
}
92114
} else {
93-
if (!item.extractedComments.includes(EXPLICIT_ID_FLAG)) {
94-
item.extractedComments.push(EXPLICIT_ID_FLAG)
115+
if (!options.explicitIdAsDefault) {
116+
if (!item.extractedComments.includes(EXPLICIT_ID_FLAG)) {
117+
item.extractedComments.push(EXPLICIT_ID_FLAG)
118+
}
95119
}
120+
96121
item.msgid = id
97122
}
98123

@@ -116,7 +141,10 @@ const serialize = (catalog: CatalogType, options: PoFormatterOptions) => {
116141
})
117142
}
118143

119-
function deserialize(items: POItem[]): CatalogType {
144+
function deserialize(
145+
items: POItem[],
146+
options: PoFormatterOptions
147+
): CatalogType {
120148
return items.reduce<CatalogType<POCatalogExtra>>((catalog, item) => {
121149
const message: MessageType<POCatalogExtra> = {
122150
translation: item.msgstr[0],
@@ -133,7 +161,11 @@ function deserialize(items: POItem[]): CatalogType {
133161
let id = item.msgid
134162

135163
// if generated id, recreate it
136-
if (!item.extractedComments.includes(EXPLICIT_ID_FLAG)) {
164+
if (
165+
options.explicitIdAsDefault
166+
? item.extractedComments.includes(GENERATED_ID_FLAG)
167+
: !item.extractedComments.includes(EXPLICIT_ID_FLAG)
168+
) {
137169
id = generateMessageId(item.msgid, item.msgctxt)
138170
message.message = item.msgid
139171
}
@@ -156,7 +188,7 @@ export function formatter(options: PoFormatterOptions = {}): CatalogFormatter {
156188

157189
parse(content): CatalogType {
158190
const po = PO.parse(content)
159-
return deserialize(po.items)
191+
return deserialize(po.items, options)
160192
},
161193

162194
serialize(catalog, ctx): string {

website/docs/releases/migration-4.md

+64-24
Original file line numberDiff line numberDiff line change
@@ -41,45 +41,85 @@ No migration steps are necessary for components provided by Lingui, such as `Tra
4141

4242
### Hash-based message ID generation and Context feature
4343

44-
The previous implementation had a flaw: there is an original message in the bundle at least 2 times + 1 translation.
44+
Starting from Lingui v4, hash-based IDs are used internally for message lookups.
4545

46-
For the line "Hello world" it'll exist in the source code as ID in i18n call, then as a key in the message catalog, and then as a translation itself. Strings could be very long, not just a couple of words, so this may bring more kB to the bundle.
46+
If you use natural language as an ID in your project, for example:
47+
```ts
48+
const message = t`My Message`
49+
```
50+
you will benefit significantly from this change. Your bundles will become smaller because the source message will be removed from the bundle in favor of a short generated ID.
4751

48-
A much better option is generating a "stable" ID based on the msg + context as a hash with a fixed length.
52+
If you use natural language as an ID, you don't need to do anything special to migrate.
4953

50-
Hash would be calculated at build time by macros. So macros instead of:
54+
However, if you use explicit IDs, like this:
5155

52-
```js
53-
const message = t({
54-
context: 'My context',
55-
message: `Hello`
56-
})
56+
```ts
57+
const message = t({id: "my.message", message: `My Message`})
58+
```
5759

58-
// ↓ ↓ ↓ ↓ ↓ ↓
60+
there are some changes you need to make to your catalogs to migrate properly. In order to distinguish between generated IDs and explicit IDs in the PO format, Lingui adds a special comment for messages with explicit IDs called `js-lingui-explicit-id`.
5961

60-
import { i18n } from "@lingui/core"
61-
const message = i18n._(/*i18n*/{
62-
context: 'My context',
63-
id: `Hello`
64-
})
62+
Here's an example of the comment in a PO file:
63+
```gettext
64+
#. js-lingui-explicit-id
65+
msgid "custom.id"
66+
msgstr ""
6567
```
6668

67-
now generates:
69+
You need to add this comment manually to all your messages with explicit IDs.
70+
71+
If you exclusively use explicit IDs in your project, you may consider enabling a different processing mode for the PO formatter. This can be done in your Lingui config file:
72+
```ts title="lingui.config.ts"
73+
import { formatter } from '@lingui/po-format'
74+
import { LinguiConfig } from '@lingui/config'
6875

69-
```js
70-
import { i18n } from "@lingui/core"
71-
const message = i18n._(/*i18n*/{
72-
id: "<hash(message + context)>",
73-
message: `Hello`,
74-
})
76+
const config: LinguiConfig = {
77+
// ...
78+
format: formatter({ explicitIdAsDefault: true }),
79+
}
7580
```
7681

82+
Enabling this mode will swap the logic, and the formatter will treat all messages as having explicit IDs without the need for the explicit flag comment.
83+
84+
You can read more about the motivation behind this change in the [original RFC](https://github.com/lingui/js-lingui/issues/1360)
85+
7786
Also, we've added a possibility to provide a context for the message. For more details, see the [Providing a context for a message](/docs/tutorials/react-patterns.md#providing-a-context-for-a-message).
7887

7988
The context feature affects the message ID generation and adds the `msgctxt` parameter in case of the PO catalog format extraction.
8089

8190
This also affects the `orderBy` with `messageId` as now the generated id is used when custom id is absent. To avoid confusion, we switched the default `orderBy` to use the source message (`message`) instead.
8291

92+
### Translation outside React components migration
93+
94+
If you have been using the following pattern in your code:
95+
96+
```tsx
97+
import { t } from "@lingui/macro"
98+
99+
const myMsg = t`Hello world!`
100+
101+
export function Greeting(props: {}) {
102+
return <h1>{t(myMsg)}</h1>
103+
}
104+
```
105+
You will need to make some changes as this is a misuse of the library that actually worked in v3.
106+
107+
Due to the changes caused by hash-based message ID feature described earlier, this approach will no longer work.
108+
109+
Instead, please use [recommended](/docs/tutorials/react-patterns.md#lazy-translations) pattern for such translations:
110+
```tsx
111+
import { t } from "@lingui/macro"
112+
import { useLingui } from "@lingui/react"
113+
114+
const myMsg = msg`Hello world!`
115+
116+
export function Greeting(props: {}) {
117+
const { i18n } = useLingui()
118+
119+
return <h1>{i18n._(myMsg)}</h1>
120+
}
121+
```
122+
83123
### Change in generated ICU messages for nested JSX Macros
84124

85125
We have made a small change in how Lingui generates ICU messages for nested JSX Macros. We have removed leading spaces from the texts in all cases.
@@ -137,13 +177,13 @@ Extractor supports TypeScript out of the box. Please delete it from your configu
137177
If your extract command looks like:
138178

139179
```bash
140-
NODE_ENV=development lingui-extract
180+
NODE_ENV=development lingui extract
141181
```
142182

143183
Now you can safely change it to just:
144184

145185
```bash
146-
lingui-extract
186+
lingui extract
147187
```
148188

149189
### Public interface of `ExtractorType` was changed

0 commit comments

Comments
 (0)