Skip to content

Conversation

@swernerx
Copy link

@swernerx swernerx commented Dec 9, 2025

Replaces the pofile dependency with pofile-ts, a modernized fork with full TypeScript support.

Why this change?

  • TypeScript-first: Native type definitions, no @types/ package needed
  • Zero dependencies: Smaller install footprint
  • Modern API: Promise-based save() method instead of callbacks
  • Maintained: pofile last release was December 2022

Affected packages:

  • @lingui/format-po
  • @lingui/format-po-gettext
  • @lingui/cli

Code improvements:

  • Use exported types (Item, Headers) directly from pofile-ts
  • Convert callback-based po.save() to Promise-based API
  • Remove manual type definitions

Other:

  • Add pofile-ts to Jest transformIgnorePatterns for ESM support
  • Add .lingui/ to .gitignore (test cache artifacts)
  • Document required yarn release:build step in CONTRIBUTING.md

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Examples update

Checklist

  • I have read the CONTRIBUTING and CODE_OF_CONDUCT docs
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)

@vercel
Copy link

vercel bot commented Dec 9, 2025

@swernerx is attempting to deploy a commit to the Crowdin Team on Vercel.

A member of the Team first needs to authorize it.

@swernerx swernerx changed the title Replaces the pofile dependency with pofile-ts refactor: replace the pofile dependency with pofile-ts Dec 9, 2025
@timofei-iatsenko
Copy link
Collaborator

FYI https://github.com/timofei-iatsenko/pofile

@vercel
Copy link

vercel bot commented Dec 9, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
js-lingui Ready Ready Preview Dec 12, 2025 8:16am

@swernerx
Copy link
Author

swernerx commented Dec 9, 2025

FYI https://github.com/timofei-iatsenko/pofile

Interesting... I was not aware of any other work on this... I assume it's related but mine might be even more up-to-date also covering a series of new test cases.

@codecov
Copy link

codecov bot commented Dec 9, 2025

Codecov Report

❌ Patch coverage is 53.12500% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.56%. Comparing base (6bb8983) to head (25058a4).
⚠️ Report is 241 commits behind head on main.

Files with missing lines Patch % Lines
packages/cli/src/services/translationIO.ts 0.00% 7 Missing ⚠️
packages/format-po-gettext/src/po-gettext.ts 71.42% 2 Missing and 2 partials ⚠️
packages/format-po/src/po.ts 63.63% 2 Missing and 2 partials ⚠️

❌ Your patch check has failed because the patch coverage (53.12%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2379      +/-   ##
==========================================
- Coverage   77.05%   76.56%   -0.49%     
==========================================
  Files          84      100      +16     
  Lines        2157     2748     +591     
  Branches      555      718     +163     
==========================================
+ Hits         1662     2104     +442     
- Misses        382      511     +129     
- Partials      113      133      +20     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@timofei-iatsenko
Copy link
Collaborator

@swernerx The background of this package is that I initially made the update for the same reasons you mentioned - to modernize it, fix the TypeScript typing issues, and generally bring it up to date. My intention, however, was to transfer it under the Lingui organization. This package is quite important for Lingui, and ideally it should be maintained by the community or the organization rather than by a random guy from the internet (in this case, me).

I faced some resistance from the Lingui maintainers on moving it into the organization, which is why the package hasn’t been published yet.

By the way, the original implementation has several known issues in Lingui repo (#2235 #2098). I still believe there’s real value in bringing this package into the organization and addressing everything together as a single project.

Another option would be completely change implementation for the https://www.npmjs.com/package/gettext-parser not sure how close this to the gettext spec though

@swernerx swernerx force-pushed the sw/migrate-to-pofile-ts branch from 08518d0 to f688222 Compare December 9, 2025 12:34
@swernerx
Copy link
Author

swernerx commented Dec 9, 2025

@timofei-iatsenko Thanks for the context! I looked at the previous approach, but I'd argue against integrating PO parsing directly into Lingui for a few reasons:

  1. Scope creep: Lingui already covers a lot of ground—SWC, Babel, macros, Vite, Metro, etc. Adding PO file parsing to that mix doesn't make it better, just bigger. Keeping concerns separated makes maintenance easier for everyone.

  2. Standalone value: We have a genuine need for a standalone PO library outside of Lingui. Others likely do too. A separate package serves that use case.

  3. Minimal migration effort: As you can see from this PR, switching to pofile-ts is trivial—just a few import changes and one API adjustment (save()writeFile() + toString()).

  4. What's new in pofile-ts: I've taken inspiration from your earlier work and modernized it further—removed the filesystem methods (pure parsing/serialization now, zero dependencies), cleaned up the source, added proper TypeScript support with exported types, and kept the same test coverage. The main goal here was simply to stop maintaining manual type definitions in Lingui.

I think keeping PO parsing as a separate, well-maintained library is the more pragmatic path forward.

@swernerx swernerx force-pushed the sw/migrate-to-pofile-ts branch from f688222 to 5da2832 Compare December 9, 2025 12:43
@timofei-iatsenko
Copy link
Collaborator

@swernerx it's not about integrating pofile into lingui, it's about hosting the package under the lingui org. So the package would be still separate, installable, but would be at full control of the lingui organization and with more trust in it from the consumers.

The problem with packages implemented by some random guy, is that, people usually don't realize how much work it would be for them after releasing something like that and burnout quite quickly leaving the package abandoned.

@swernerx
Copy link
Author

swernerx commented Dec 9, 2025

Fair point about maintainer burnout—I've been doing open source for years and know exactly what you mean. That said, I'd suggest a step-by-step approach here:

  1. This PR is low-risk: Switching to pofile-ts is trivial and doesn't make anything worse. It just modernizes a dependency that hasn't been updated in two years.

  2. Future integration is possible: If the Lingui team later decides to bring PO parsing under the organization—in whatever form—I'm open to that conversation. But that decision doesn't need to block this straightforward dependency update.

  3. Lingui already depends on many external packages: The PO parser is important, sure, but it's one of dozens of dependencies. Babel, SWC, Vite plugins, Metro transformer, messageformat packages—all maintained by "some random person" at some level. The same trust argument could apply to any of them.

  4. It's all open source: Since everything is MIT licensed, the Lingui team can fork, integrate, or adopt the code at any time—no alignment or permission needed. That decision is entirely yours to make whenever it makes sense.

Let's not let perfect be the enemy of good. This PR improves the status quo today. Organizational decisions about package ownership can happen separately.

@timofei-iatsenko
Copy link
Collaborator

@swernerx meanwhile maybe you can work on these problems in your fork:


Multiline formatting issue: #2235

Here i would suggest to have a wrap or fold option similar to gettext-parser's https://www.npmjs.com/package/gettext-parser#:~:text=with%20possible%20values-,foldLength,-is%20the%20length

foldLength is the length at which to fold message strings into newlines (default: 76). Set to 0 or false to disable folding.

This later might be exposed as the formatter parameter.

Also would be nice to dig into original source and specs to understand why pofile creates an empty string when multiline message:

msgid ""
"Something\n"
"or something else"

This change might be a disruptive a bit, i would not consider it's breaking because, files generated would be still consumable and result should not be changed, but changes here will definitely produce a diff in consumers codebases.


PO translator comments must begin with # and space #2098

This should be easy to fix. Will also produce diff but not breaking.

@swernerx
Copy link
Author

@swernerx meanwhile maybe you can work on these problems in your fork:

Multiline formatting issue: #2235

Here i would suggest to have a wrap or fold option similar to gettext-parser's https://www.npmjs.com/package/gettext-parser#:~:text=with%20possible%20values-,foldLength,-is%20the%20length

foldLength is the length at which to fold message strings into newlines (default: 76). Set to 0 or false to disable folding.

This later might be exposed as the formatter parameter.

Also would be nice to dig into original source and specs to understand why pofile creates an empty string when multiline message:

msgid ""
"Something\n"
"or something else"

This change might be a disruptive a bit, i would not consider it's breaking because, files generated would be still consumable and result should not be changed, but changes here will definitely produce a diff in consumers codebases.

PO translator comments must begin with # and space #2098

This should be easy to fix. Will also produce diff but not breaking.

Indeed my fork had the same issue. I fixed that and also implemented two new options to tweak it as needed for different usage models.

Fixes: #2379 (hopefully) ;)

Comment on lines +131 to +136
* When `false`, uses GNU gettext's traditional format with an empty first line:
* ```po
* msgid ""
* "First line\n"
* "Second line"
* ```
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have any links to read about this traditional format? I'm wondering was it a indeed a traditional and made intentionally by author of the pofile or it was done unintentionally and we keeping them unnecessarily

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty string convention for multiline entries in PO files is documented in the GNU gettext manual, section "The Format of PO Files":

Link: https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html

The format uses C-style string literal concatenation — adjacent quoted strings are automatically joined. The empty string "" on the first line is a stylistic convention that:

  • Improves alignment of subsequent lines
  • Enhances readability
  • Has no semantic effect (empty string + rest = rest)

The underlying mechanism comes from the C specification (ISO C99 Section 6.4.5 "String literals" and 5.1.1.2 "Translation phases", Phase 6).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For reference the exact example with detailed description is here
https://www.gnu.org/software/gettext/manual/html_node/More-Details.html#Further-details-on-the-PO-file-format

What i'm thinking, is that if this pattern is stated in the official docs - this should be a default, not the "compact" style, wdyt?

I would also put a link to this doc into the flag description.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a bit undecided here. My thinking was mostly shaped by the Crowdin use case and other automated tooling I’ve worked with recently – for those tools the concrete PO formatting is effectively irrelevant. Since Crowdin is one of the bigger players, I considered its behavior a reasonable “de-facto” default. In our setup people almost never open the PO files manually and 99.9% of translations happen fully automatically, so readability of multiline entries hasn’t really been a concern so far.

That said, if the GNU-style empty-string convention is explicitly recommended in the official docs and improves readability for anyone who does look at the files, I’m perfectly fine with using that as the default and treating the compact form as an alternative style.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since Crowdin is one of the bigger players, I considered its behavior a reasonable “de-facto” default

I would consider a deafult example written in the gettext official docs.

In our setup people almost never open the PO files manually and 99.9% of translations happen fully automatically, so readability of multiline entries hasn’t really been a concern so far.

Same here.

@timofei-iatsenko
Copy link
Collaborator

@swernerx both po formatters has this ugly piece of code:

       // accessing private property
        ;(po as any).headerOrder = Object.keys(po.headers)

The as any assertion is no longer needed with your fork. Could you change it, please?

@swernerx
Copy link
Author

@swernerx both po formatters has this ugly piece of code:

       // accessing private property
        ;(po as any).headerOrder = Object.keys(po.headers)

The as any assertion is no longer needed with your fork. Could you change it, please?

Fixed

@timofei-iatsenko
Copy link
Collaborator

@swernerx now you need to update and fix conflicts from the main branch.

Tests fail without running yarn release:build first, as @lingui/cli
and other packages need to be compiled before they can be resolved.
- Replace pofile dependency with pofile-ts ^2.2.0
- Use exported types (Item, Headers) instead of manual definitions
- Use fs.writeFile + po.toString() instead of removed po.save()
- Add pofile-ts to Jest transformIgnorePatterns for ESM support
- Add .lingui/ to .gitignore (test cache artifacts)

pofile-ts is a modernized fork with TypeScript support and zero
dependencies (pure parsing/serialization, no filesystem access).
BREAKING CHANGE: pofile-ts v3.0.0 uses a functional API instead of classes

Migration:
- PO.parse() → parsePo()
- po.toString() → stringifyPo(po)
- new PO() → createPoFile()
- new PO.Item() → createItem()
- PO class → PoFile interface
- Item class → PoItem interface
Upgrade pofile-ts to v3.1.0 and expose new serialization options:

- foldLength: Maximum line width before folding (default: 80, 0 = disable)
- compactMultiline: Compact format for multiline strings (default: true)

These options enable better compatibility with translation platforms
like Crowdin that may have issues with empty first lines in multiline
strings.

Refs: lingui#2235
headerOrder is a public property on PoFile, no cast needed.
@swernerx swernerx force-pushed the sw/migrate-to-pofile-ts branch from 77290d4 to 65860a1 Compare December 10, 2025 17:20
@swernerx
Copy link
Author

@swernerx now you need to update and fix conflicts from the main branch.

Done

@timofei-iatsenko
Copy link
Collaborator

@swernerx it's failing on the tests now.

I'm wondering may be you can also look at the performance side of that pofile parsing? We had a discussion #2320 (comment) some time ago which you might be interested to follow. TL; DR; When you have a quite big catalog parsing is taking a significant time.

I'm not sure is it something that could be improved a lot, but at least you could give it a try.

@timofei-iatsenko
Copy link
Collaborator

@swernerx CI still failing

@timofei-iatsenko
Copy link
Collaborator

@swernerx Is there anything to add or we can review and move this forward?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants