-
Notifications
You must be signed in to change notification settings - Fork 382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(core/i18n/t): Precise typing for message parameters #1840
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
size-limit report 📦
|
So impressed about what typescript type system is capable of. However, the primary and recommended usage of lingui is via Macro (this is the only way to receive all goodies from lingui) which is literally eliminating the issue: const username = 'Dmitry';
t`Hello ${username}` // <-- extracted as `Hello {username}` I really like what you did and appreciate the effort, but, i'm a bit concerned about maintenance burden and complexity of the solution for a little value (considering primary macro usecase). |
First and foremost, I want to extend my sincere gratitude for your time and the insightful feedback you provided. I appreciate your point about the primary usage of Lingui being via the Macro system. However, I'd like to share some context regarding my use case, which I believe might shed light on why the enhancements proposed in the PR could be beneficial to a subset of Lingui users, particularly those working on server-side applications. As a developer of a server-side application, my choice to use Lingui was driven because it's written in TypeScript and the simplicity offered by the I understand that the Macro system is the recommended approach. However, as a user, my priority is to adopt solutions that align with the project's needs and technical constraints. For my use case, employing a pure TypeScript compiler (TSC) without introducing additional bundlers like Vite or SWC is paramount. The necessity to integrate these tools solely for Macro processing introduces a layer of complexity and potential technical overhead that I am keen to avoid. Furthermore, a glance at the npmjs statistics indicates a noteworthy trend: The two full working days I invested in crafting this solution were not due to a lack of awareness of the Macro system. Instead, it was a deliberate decision to enhance the typing mechanism within the core package to cater to use cases like mine, where the introduction of additional bundlers (limited by lingui with just Babel and SWC) is not an optimal path. While I understand the concerns about the maintenance burden and complexity of the solution, I believe this enhancement has the potential to serve a meaningful segment of the Lingui community. It offers a way to leverage the library's powerful features while accommodating different project setups and developer preferences. Once again, thank you for considering this contribution. I am looking forward to your thoughts and guidance on the next steps. |
Also, I've been exploring various solutions, including typesafe-i18n, which offers an intriguing approach to i18n, aligning closely with my requirements. It emphasizes type safety and simplicity, leveraging a pure CLI without the need for bundlers or transpilers. However, despite its promising features, The popularity of My contribution aims to bridge this gap, ensuring that Lingui not only maintains its robust features and developer-friendly approach but also caters to the evolving needs of the wider developer community. |
BTW, while the higher download count of After the unfortunate realization that I couldn't use By incorporating these typing enhancements, Lingui has the potential to position itself as a unique and compelling choice in the i18n market. It's not just about retaining the existing user base but also about attracting those who might have overlooked Lingui due to the perceived complexity of achieving type safety. This enhancement could be a game-changer, significantly broadening Lingui's appeal and encouraging broader adoption. I firmly believe that these enhancements will not only meet the current users' needs but also resonate with a wider audience looking for an i18n library that marries functionality with ease of use and type safety. |
Ahh, now i see where this idea came from. Well, as i mentioned i'm not against your changes and always striving to the most type-safety in my projects, just raised a concern about maintainability. It isn't a secret that this particular kind of typings are extremely hard to read and understand and not many developers can and would do that. So in the end of the day we may stuck with something broken and no one understand what's going on and how to fix it. |
Regarding the PR, we're using tsd for type testing. I think you should add more tests covering happy and unhappy paths with typings. You could check |
And also would be nice to update website and highlight this feature. |
Thank you @thekip for your supportive feedback and for the opportunity to contribute to this exciting enhancement. I'm glad to hear that we're moving in the right direction. Regarding your suggestions:
|
I think @andrii-bodnar and myself could help with that. I think in general we could start by adding a separate page "Using in backend" and explain how to use without macro, what are strong sides and so on. Furthermore, I think we could add "fully type safe" as a highlight on the index page between other highlight there. |
packages/core/src/i18n.t.ts
Outdated
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { Replace, Simplify, Trim, UnionToIntersection } from "type-fest" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you use webstorm and see warning from eslint from this rule that's probable because webstorm's eslint service should be restarted. It doesn't pick up changes in package.json automatically and continues to show error even if the package is already added. However this is not the case when you run eslint command directly.
…e string type for a message
I'm excited to share some additional enhancements I've made Strict Typing for FormattersIn pursuit of providing a more robust and error-resistant internationalization experience, I have implemented strict typing for formatters. This means that the library is now even more intelligent in understanding and enforcing the types of variables passed to formatters. Previously acceptable but erroneous usages like this are now rightly identified as errors during compilation. i18n._("You have {n, number} unread messages", {n: 'hello'}) Correct usages such this are seamlessly validated, ensuring that the data types align with the expectations of the number formatter. i18n._("You have {n, number} unread messages", {n: 10}) This enhancement is not limited to number formatters but extends to all formatters. Types for formatter inputs are automatically inferred from the actual formatter types. This means there is a single source of truth for the expected types, minimizing maintenance overhead and eliminating the need for manual synchronization. The system is smart enough to understand and enforce the types directly based on the formatters themselves. Comprehensive Testing and Bug FixesFollowing your valuable suggestion, I've expanded the test suite, adding more cases to ensure the robustness of the new typing mechanism. This testing process helped unearth and rectify several bugs, further solidifying the reliability of the implementation. I'm open to suggestions if there are any specific cases or scenarios you think might need additional attention. Thank you once again for your support and guidance in this endeavor. |
Regarding the tests, i consciously chose not to overload the test suite with cases involving deliberately incorrect message syntax. This is because, in runtime, such syntax should rightfully throw errors, and during compile-time, our capacity to convey formalized error messages is somewhat limited. Consequently, I believe that encountering unpredictable parsing results at compile-time with inherently incorrect syntax is an acceptable trade-off. |
I completely agree that reliability in software often necessitates a more complex underpinning than its less dependable counterparts. Yet, I've made a concerted effort to ensure that the complexity of this solution is manageable and not as daunting as it might initially appear. I've endeavored to make the code as comprehensible as possible within the syntax constraints of TypeScript. This includes giving types, generics and variables meaningful names and decomposing the parser into the smallest feasible modules, each with a clear and descriptive name. The intention here is to make the logic as transparent and approachable as possible, even for developers who might not be deeply versed in TypeScript's advanced type system.
In the event that any issues arise, or if certain aspects of the implementation are unclear, I want to extend an open invitation for direct communication. Feel free to reach out to me on Telegram, and I will do my utmost to provide clarity or assistance. |
… type ExtractBraceBody
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #1840 +/- ##
==========================================
- Coverage 76.39% 76.36% -0.04%
==========================================
Files 81 81
Lines 2072 2073 +1
Branches 529 530 +1
==========================================
Hits 1583 1583
Misses 377 377
- Partials 112 113 +1 ☔ View full report in Codecov by Sentry. |
For beautiful "Inlay Hints" for Parameter names in IDE. Previously IDE showed "id" even when you called it with descriptor. Now it doesn't show the first parameter name at all, which is more logical
I renamed parameter |
This looks impressive but I agree with @thekip's concern about maintenance burden. Personally, I believe that it's a good candidate for a new feature of the Lingui ESLint plugin instead of bringing such complexity into the library core. For example, as it is done in the eslint-plugin-formatjs - https://formatjs.io/docs/tooling/linter/#enforce-placeholders Or we can use the declaration merging and module augmentation features of TypeScript and keep it as a separate package. In addition, it contains breaking changes that can't be released without bumping a major release, which we'd like to avoid for now. |
@andrii-bodnar addressing your concerns, I'd like to present a structured argument against that perspective:
This pull request represents a significant step forward in making lingui a unique and pioneering product in the TypeScript i18n space, and possibly beyond. It's about enhancing type safety in our applications, a core principle that no one manages better than TypeScript. I kindly request a reconsideration of the advantages offered by this approach. You will get a unique product on the market, i will get i18n type safety for my server-side project:) |
Oh, and a regarding the ESLint plugin, i don't think that an ESLint-based approach can achieve 100% functionality of this TypeScript-based solution. This is because my solution inherently considers the data types of formatters, i'm not sure it can be fully replicated in ESLint without duplicating data types in both TS compile-time and ESLint runtime, which violates the principle of a single source of truth. For instance, specific formatters like |
It's a breaking change since the exported types have changed ( We appreciate the effort, but let's consider a compromise solution as a separate package or an Eslint-based option. There is probably no need for such precise type validation. We are trying to keep the balance between simplicity and usability, and this PR definitely adds more complexity. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the effort! I'm a fan of type safety but agree that this adds a lot of complexity.
It'll make it harder for TS-non-experts to contribute.
That being said, I'd love to have some compromise, maybe declaration merging could help?
https://www.typescriptlang.org/docs/handbook/declaration-merging.html
Also, I might be wrong, but it seems you're writing your responses with the help of some AI. Can you instruct it to write more concise messages? The responses feel a bit too long. Thank you!
| ExtractVars<Next> // Recursively process the rest of the string after the current variable. | ||
: // If not a formatter, create a type with a property where the key is the whole trimmed BraceBody and the value is a string | ||
{ [P in Trim<BraceBody>]: string } | ExtractVars<Next> // and recursively process the rest of the string. | ||
: {} // If the string does not contain a valid structure, return an empty object type (possibly we can return an error here, because this branch indicates parsing error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{}
does not represent an "empty object" in TS, so ExtractVars
might be behaving differently from what you expect
see https://www.totaltypescript.com/the-empty-object-type-in-typescript
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, thank you for pointing that out! I know about this specific issue, and it's actually the only place I also wasn't sure about :) In this context, it makes no difference because ExtractVars
only applies if the input string contains interpolation. But in the general case, I should use never
here, and it should be fixed, of course. Thank you again!
Hello @lingui/core enthusiasts!
I'm thrilled to introduce a groundbreaking enhancement that's set to transform your experience with the
i18n.t()
function. This PR is all about injecting steroids into type safety, ensuring that every parameter in your translation strings is strictly typed and meticulously validated. Say goodbye to loose strings and hello to precision!Key Changes:
Strict Parameter Validation:
i18n.t()
with a single string argument only if the string does not contain parameters. For example:Advanced Parameter Extraction:
I18nT
type, parameters are extracted directly from the string, paving the way for a sophisticated, error-proof translation workflow. For instance:Results in
The Magic Behind:
Your Feedback is Our Catalyst:
This enhancement is a testament to our commitment to excellence and innovation. But it's your insights, feedback, and active participation that will fine-tune this feature to perfection. Dive into the code, test it out, and let's make
i18n.t()
not just a function, but a powerhouse of precision and reliability.I'm eagerly awaiting your valuable feedback and suggestions!