Skip to content
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(macro): support transforming member expressions to named args #1874

Closed
wants to merge 1 commit into from

Conversation

dan-dr
Copy link
Contributor

@dan-dr dan-dr commented Mar 6, 2024

Description

Helps extract complex member expressions to named arguments with _ delimiter.
so t`Very {result.cool} Stuff` results in Very {result_cool} Stuff

I wanted to add this because we have a lot of results from APIs that I really don't want to break down to different variables.

WIP, wanted to see thoughts of maintainers

TODO:

  • Add opt-in option in the lingui config, because it will break previous extractions. name suggestion?
  • Update tests
  • Docs

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)

Copy link

vercel bot commented Mar 6, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
js-lingui ✅ Ready (Inspect) Visit Preview 💬 Add feedback Mar 6, 2024 1:26am

@dan-dr dan-dr changed the title support transforming member expressions to named args feat(macro): support transforming member expressions to named args Mar 6, 2024
Copy link

github-actions bot commented Mar 6, 2024

size-limit report 📦

Path Size
./packages/core/dist/index.mjs 2.86 KB (0%)
./packages/detect-locale/dist/index.mjs 723 B (0%)
./packages/react/dist/index.mjs 1.67 KB (0%)
./packages/remote-loader/dist/index.mjs 7.26 KB (0%)

@timofei-iatsenko
Copy link
Collaborator

I thought about something more generic, what about adding a special macro ph or var to manually specify the name of the placeholder?

t`Very ${ph(result.cool, 'myName')} Stuff` // -> Very {myName} Stuff

This would also work for evey other type of the expressions:

// call expression
t`Very ${ph(callExpression(), 'myName')} Stuff`

// this expression
t`Very ${ph(this.myVar, 'myName')} Stuff`

Does it make sense?

@dan-dr
Copy link
Contributor Author

dan-dr commented Mar 6, 2024

I think that kinda ruins readability and DX, the variable identifier chain should already be descriptive enough.
What we can do is have the option to specify depth. So for example depth 2 will output name_first when given user.name.first

@timofei-iatsenko
Copy link
Collaborator

By the way,

this is valid JS:

 {
  'app.foo.bar': "World"
}

As well as this is valid ICU:

Hello, {app.foo.bar}!

So may be if it's a MemberExpression just get it as is, without replacing . to _ and other modifications?

t`Very ${this.myVar} Stuff` // -> Very {this.myVar} Stuff

@timofei-iatsenko
Copy link
Collaborator

Additionally this change will affect:

@dan-dr
Copy link
Contributor Author

dan-dr commented Mar 6, 2024

So may be if it's a MemberExpression just get it as is, without replacing . to _ and other modifications?

right, can be done. maybe also should be configured. I was thinking translators will have easier time with underscore.

btw my implementation ignores the this part

regarding the rule, I think it shouldn't effect it? developers should decide if they want it or not, especially since this will be opt-in due to breaking change.

what's the stance on swc implementations? it's already diverging due to the useLingui macro we did, but will there be an official release without swc implementation? Is there any knowledgable rust maintainer that can port it?

@timofei-iatsenko
Copy link
Collaborator

right, can be done. maybe also should be configured. I was thinking translators will have easier time with underscore.

less configuration, less branching, less maintenance burden. I don't see any benefit of _ instead of original . (dots)
The translation platform should do quality checks and helps with variables for translators.

btw my implementation ignores the this part

i saw this, also I think it's obvious and expected from developers point of view if this {app.foo.bar} extracted as is, why {this.foo.bar} is not? Should be consistently.

regarding the rule, I think it shouldn't effect it? developers should decide if they want it or not, especially since this will be opt-in due to breaking change.

It should affect. The purpose of this rule is enforcing developers NOT to write expressions (including member expressions) in the messages due to lack context information and potential poor quality of the translation. If member expression will be processed as proposed in this PR, this rule should be relaxed to skip such cases. Or configuration added, but i prefer to keep them in sync, so the latest version of eslint plugin is aligned with latest version of the lib.

what's the stance on swc implementations? it's already diverging due to the useLingui macro we did, but will there be an official release without swc implementation? Is there any knowledgable rust maintainer that can port it?

I'm the author of that swc implemetation. The useLingui is opt-in hook, where change from this PR will affect essential macros.

I would prefer to not do a lot of configuration and just include it in lingui 5 as default. SWC implementation should also be ready till that moment.

@andrii-bodnar andrii-bodnar marked this pull request as draft March 6, 2024 12:30
@andrii-bodnar
Copy link
Contributor

@dan-dr thank you for the contribution!

I see a couple of threats here:

  • What if we have a few of the same strings with different variables passed? Will this result in duplicate strings with different placeholders and be treated as separate strings during extraction? If so, this could lead to excessive translation effort, as translators will translate these strings separately.
  • In case the variable changes for any reason (renaming, refactoring, etc.), the string will be different. As a result, the previous translations may be lost (TM pre-translate with placeholder auto-substitution should then be applied, otherwise this string will be translated again and it will lead to additional translation costs).

@timofei-iatsenko
Copy link
Collaborator

@andrii-bodnar all these points are true for current implementation as well. This PR doesn't bring something new, just extending the cases wich macro / extractor could process.

There is a tradeoff between passing more information to translator and re-usability of the message.

Angular's team, for example, solves this problem a bit differently, they replace arguments in the string into positional placeholders but also saves meta information how these placeholders are mapped. Something like:

# ph1: app.foo.bar
# ph2: myFunction()
msgid: Hello {ph1} and {ph2}

So translators could get the point of what variable is in here by looking into metadata.

Currently, if developer want to reuse the string, he should provide the same name for all placeholders, sometimes this might require to introduce an intermediate variable for that:

t`Hello ${userName}` // -> Hello {userName} 
t`Hello ${getUserName()}` // -> Hello {0}


// Developer may reuse the translation by introducing a variable:
const userName = getUserName();
t`Hello ${userName}`  // ->  Hello {userName}  match to the string from case 1

So, maybe uncouple variable name from placeholder name not that bad idea even considering worse DX.

In my example if developer will specify a placeholder name it would be stable between refactorings and would be more explicit than using a variable name

t`Hello ${ph(getUserName(), 'userName')}`
t`Hello ${ph(userName, 'userName')}`

By the way, Angular's team solves this issues as well, they have introduce a special magic syntax for that, which is for my test is worse than using additional macro function

$localize`Hello ${getUserName()}:userName` // `:userName` after the interpolation is a special "magic" syntax used in Angular to give name for placeholders

@timofei-iatsenko
Copy link
Collaborator

Just have got an idea:

using this syntax to give a name for a placeholder

t`Hello ${{userName: getName()}}`

Maybe a little bit weird, but simply fewer symbols to write and minus one import on the top.

@dan-dr
Copy link
Contributor Author

dan-dr commented Mar 8, 2024

I like it a bit more, JS-y. will make it optional. I agree on the eslint rule btw.
So the general thought it to make this a v5 feature? Really don't want to maintain a fork :D

@timofei-iatsenko
Copy link
Collaborator

I thought about that for a while and here is my conclusion:

  • We can make what you propose, and include in v4 with an additional configuration flag, disabled by default. This flag would be true by default in lingui5 (and even flag itself might be deleted)
  • What I proposed (t`Hello ${{userName: getName()}}`) is a nice to have, addition and could be implemented as a separate feature in other PR.

In v5 we can make a general configuration which will turn on/off using variable name in placeholder:

// useVaraibleNameAsPlaceholder: true (default)
t`Hello ${userName}` // -> Hello {userName} 
t`Hello ${user.name}` // -> Hello {user.name} 

// useVaraibleNameAsPlaceholder: false
t`Hello ${userName}` // -> Hello {0} 
t`Hello ${user.name}` // -> Hello {0} 

This has more sense than enabling/disabling particular syntax processing. Those users who want as much string as possible to be reused could opt out from named placeholders, or use explicit placeholders syntax t`Hello ${{userName: getName()}}`

The member expression parts should go as-is to the placeholder, no need to replace dots to dashes or cutting levels.
Test cases:

t`Very ${this.myVar} Stuff` // -> Very {this.myVar} Stuff
t`Hello ${user.name}` // -> Hello {user.name}

// any level deeper should be supported
t`Hello ${foo.bar.baz}` // -> Hello {foo.bar.baz}

// don't forget about JSX
<Trans>Hello {foo.bar.baz}</Trans>  // -> Hello {foo.bar.baz}

Also rebase your branch to the next base, there is massive refactoring of the macro code.

@dan-dr
Copy link
Contributor Author

dan-dr commented Mar 8, 2024

Question: How do translation platforms usually deal with arg changes?
example: Text with {0} that turns to Text with {var}

Because I think that in v5 you really should make sure people can upgrade and keep the same "behavior". in what you propose, if someone is currently using both userName and user.name, they will have no way to upgrade to v5 without it causing changes in their messages. That should be avoided imo.

@timofei-iatsenko
Copy link
Collaborator

That's why it's planned for v5. We could not go forward if we would need to keep all this backward compatibility.

Translation platforms will usually treat these as two separate strings, but with help of Translation Memory the translation could be retrieved back pretty quick.

@timofei-iatsenko
Copy link
Collaborator

@andrii-bodnar also not a breaking change if would be under the config flag

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.

3 participants