symfony / ux

Symfony UX initiative: a JavaScript ecosystem for Symfony
https://ux.symfony.com/
MIT License
861 stars 315 forks source link

[TwigComponent] Scope of translation domains #2047

Open CMH-Benny opened 3 months ago

CMH-Benny commented 3 months ago

There is something unexpected going on with scopes in twig UX components, when it comes to translation domains. Let's assume there is a template foo.html.twig and it uses a component twig:Bar that allows to pass content to it:

{% trans_default_domain "foo" %}
<div class="content">
  <p>{{ "foo.whatever"|trans }}
  <twig:Bar baz="bla">
    <p>{{ 'foo.bar'|trans }}</p> {# |trans will use default "messages" domain here and not "foo" that I declared in this template #}
  </twig:Bar>
</div>

To make it work, I have to do:

{% trans_default_domain "foo" %}
<div class="content">
  <p>{{ "foo.whatever"|trans }}
  <twig:Bar baz="bla">
    {% trans_default_domain "foo" %}
    <p>{{ 'foo.bar'|trans }}</p> {# |trans will now use "foo" domain correctly #}
  </twig:Bar>
</div>

Is that intended? If you want to render multiple twig:Bar and have to repeat the setting again and again. You also don't necessarily want to put it inside of the template of the component, because it might be used in another outer template where the content uses texts from another translation domain. Is there a way to avoid that, because it's pretty unintuitive.

Thank you!

smnandre commented 3 months ago

This is how trans_default_domain works: only in the current scope.

This behaviour is similar if you use "embed" instead of components (this is more or less the same isolation logic)

CMH-Benny commented 3 months ago

Thanks for your reply ❤️ That makes sense, but imagine this:

{% trans_default_domain "foo" %}
<div class="content">
  <p>{{ "foo.whatever"|trans }}
  <twig:Bar baz="bla">
    {% trans_default_domain "foo" %}
    <p>{{ 'foo.bar'|trans }}</p>
  </twig:Bar>
  <twig:Headline level="1">
    {% trans_default_domain "foo" %}
    {{ 'foo.bar'|trans }}
  </twig:Headline>
  <twig:Paragraph>
    {% trans_default_domain "foo" %}
    {{ 'foo.test'|trans }}
  </twig:Paragraph>
  <twig:Button purpose="primary">
    {% trans_default_domain "foo" %}
    {{ 'foo.action'|trans }}
  </twig:Button>
</div>

TwigComponents suggest you can use them similar as HTML tags, but suddenly you have to add boilder plate to work with simple things, like translations or includes. I understand the limitation, but it leads to an unpleasent DX.

Don't get me wrong, I love the whole symfony ux project! It's really amazing already, but things like that, suggests to me, that it might not be the best solution to pick in the long run. If things like that can't get sorted out over the time at least. Templates are also ment to be used by designers as well in some cases, if they now need to learn complex twig shenanigans, the whole idea gets kinda lost.

Don't get this as critique on specific implementations, or on anyone who is actively working on this. What you are doing is amazing! I imagine it's pretty hard to build such a thing on top of existing code that was never ment to work like that, so full acknowledgement on that.

But I still thought it's worth noting that simple things like this can make it or break it for some people. Even tho I really love UX, I slightly regret starting to use it, because it feels like it's not ready for production yet when it comes to some details. This is not the first time I hit a wall I have to workaround.

smnandre commented 3 months ago

Thank you for the nice message ;)

Some comments (some more opiniated/personal than others):

you have to add boilder plate to work with simple things, like translations or includes

Could you elaborate on "includes" ? Maybe we can do something here :)

TwigComponents suggest you can use them similar as HTML tags

Could you tell me where you feel it is suggested? (genuine question)

CMH-Benny commented 3 months ago

Hey, thanks for the response :)

  • As i said, this is the behaviour or the trans_default_domain tag, so nothing we can do here (and the same code you gave, with embed tags, would require twice the amount of code so i guess it's not that bad 😅 )

That's true, the Syntax is already much better, it's probably just a matter of knowledge, if you see it as a pure replacement for embeds, then it's a different thing than a fully fledged HTML Component Replacement. I mean React, Angular, Vue do also have some kind of abstraction like JSX and such, but with less hassle of scopes mostly.

  • Feel free (really) to open an issue in the symfony/symfony repository to suggest some changes about it

Yeah, but I am not sure what the suggestion would be in this case, I mean usually that scoping is fine if you work with classic twig features, it only becomes a littly clunky with the twig components, because it's not straight forward that inside of a thing, looking like n HTML tag, you're suddenly in a different scope. While some things like variables are available, some others like that domain setting and probably form theme settings are not.

  • Twig components are ... Twig components
  • I don't think there ever was an intention to "leave Twig" (which is really an amazing template engine)

Leaving Twig would be indeed a huge step and I also wouldn't be sure if that makes sense. Twig is pretty cool overall, but it comes with some concepts that may limit the idea of the components. I mean, if I create a Button Component, of coure I want to use it in multiple places and if i seperate things and have multiple translation files for different sections of the application, I can't set a fixed domain for the template, so I have to put it inside every Button content that I want to have a translated text on, based on where the Button is. I mean this follows a clear logic, but it also is somehow unexpected if I am in a template that is configured to be in this domain already. The syntax just doesn't suggest that you're in a fully isolated scope out of a sudden - If you know that, you can deal with that easily, but it's confusing for people not familar with this concept at all :)

Could you elaborate on "includes" ? Maybe we can do something here :)

Ah, that was the wrong word, actually I ment blocks, not includes - Sorry for that. And even tho we have access to blocks by using outerBlocks, I faced situations, when working in templates that overwrites blocks and I added components and forwarded some othr blocks it refused to render at all, The only way around it was having a variable thatt holds the content of a block and then put that variable as content of the component with |raw filter.

Could you tell me where you feel it is suggested? (genuine question)

Purely by the Syntax, actually. It's not like the headline of twig Components is: "A fully fledged HTML Component Solution for PHP" or something like that, but if you see a template like the examples above:

{% trans_default_domain "foo" %}
<div class="content">
  <p>{{ "foo.whatever"|trans }}
  <twig:Bar baz="bla">
    {% trans_default_domain "foo" %}
    <p>{{ 'foo.bar'|trans }}</p>
  </twig:Bar>
  <twig:Hero>
    <twig:Headline level="1">
      {% trans_default_domain "foo" %}
      {{ 'foo.bar'|trans }}
    </twig:Headline>
    <twig:Paragraph>
      {% trans_default_domain "foo" %}
      {{ 'foo.test'|trans }}
    </twig:Paragraph>
    <twig:Button purpose="primary">
      {% trans_default_domain "foo" %}
      {{ 'foo.action'|trans }}
    </twig:Button>
  </twig:Hero>
  <twig:Card>
    <twig:Headline level="1">
      {% trans_default_domain "foo" %}
      {{ 'foo.bar'|trans }}
    </twig:Headline>
    <twig:Paragraph>
      {% trans_default_domain "foo" %}
      {{ 'foo.test'|trans }}
    </twig:Paragraph>
    <twig:Button purpose="primary">
      {% trans_default_domain "foo" %}
      {{ 'foo.action'|trans }}
    </twig:Button>
  </twig:Card>
  <twig:Headline level="1">
    {% trans_default_domain "foo" %}
    {{ 'foo.bar'|trans }}
  </twig:Headline>
  <twig:Paragraph>
    {% trans_default_domain "foo" %}
    {{ 'foo.test'|trans }}
  </twig:Paragraph>
  <twig:Button purpose="primary">
    {% trans_default_domain "foo" %}
    {{ 'foo.action'|trans }}
  </twig:Button>
</div>

It looks like you cna mix and match your components easily, but you would expect to have properties of the hero or the card available in the child components, be it the translation domain or some setting or text property for the Button Content etc. you can do some of that via <twig:block> here and there, but I guess this Syntax might gave me a whole wrong idea on what those components actually are. So I wonder, if that assumtion is wrong, what should I use instead to achive that? If I use React for example, why have twig components then?

smnandre commented 3 months ago

Thank you very much for all these answers, really instructive. I'll answer more deeply when i have time, but i can maybe unlock you with a precision.

Twig components (both anonymous and class-based) can be simple (and very efficient) or similar to embed blocks in Twig (with similar constraints you experienced).

I feel many of your components could be simpler / easier to read if you passed them props instead of blocks.

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Headline level="1" title="{{ 'foo.bar'|trans }}" />
  <twig:Paragraph content="{{ 'foo.test'|trans }}" />      
  <twig:Button purpose="primary" label="{{ 'foo.action'|trans }}" />
</twig:Card>

Already better no ?

CMH-Benny commented 3 months ago

Thank you very much for all these answers, really instructive. I'll answer more deeply when i have time, but i can maybe unlock you with a precision.

Thank you as well for your answer and I am looking forward to your deeper reply :)

Twig components (both anonymous and class-based) can be simple (and very efficient) or similar to embed blocks in Twig (with similar constraints you experienced).

Exactly and they do really shine if they are simple 🙌 But when it comes to the complex ones, it becomes tricky 😉

I feel many of your components could be simpler / easier to read if you passed them props instead of blocks.

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Headline level="1" title="{{ 'foo.bar'|trans }}" />
  <twig:Paragraph content="{{ 'foo.test'|trans }}" />      
  <twig:Button purpose="primary" label="{{ 'foo.action'|trans }}" />
</twig:Card>

Already better no ?

Well, yes. But let me elaborate the process from here:

We have a simple component now for a Button that gives me purpose and label attributes, cool.

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Button purpose="danger" label="{{ 'foo.action1'|trans }}" />
  <twig:Button purpose="warning" label="{{ 'foo.action2'|trans }}" />
  <twig:Button purpose="primary" label="{{ 'foo.action3'|trans }}" />
  <twig:Button purpose="info" label="{{ 'foo.action4'|trans }}" />
</twig:Card>

Now we start using the Buttons and we discover we need Icons. So we add another (optional) attribute, icon that takes the icon name and renders the UX-Icon internally prefixing the label. Amazing.

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Button purpose="danger" label="{{ 'foo.action1'|trans }}" icon="times" />
  <twig:Button purpose="warning" label="{{ 'foo.action2'|trans }}" icon="save" />
  <twig:Button purpose="primary" label="{{ 'foo.action3'|trans }}" icon="eye" />
  <twig:Button purpose="info" label="{{ 'foo.action4'|trans }}" />
</twig:Card>

Now we want Dropdown Buttons, so it needs another trailing-icon or isDropdown (if there is no other case for a trailing icon) Attribute... Or we would just put them as Content, then it's fully flexible what the Buttons Content is. So it's:

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Button purpose="danger" label="{{ 'foo.action1'|trans }}" icon="times" trailing-icon="caret-down" />
  <twig:Button purpose="warning" label="{{ 'foo.action2'|trans }}" icon="save" :isDropdown="true" />
  <twig:Button purpose="primary" label="{{ 'foo.action3'|trans }}" icon="eye" />
  <twig:Button purpose="info" label="{{ 'foo.action4'|trans }}" />
</twig:Card>

VS

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:Button purpose="danger">
     {% trans_default_domain "foo" %}
     <twig:Icon name="times" /> {{ 'foo.action1'|trans }} <twig:Icon name="caret-down" />
   </twig:Button>
   <twig:Button purpose="warning">
     {% trans_default_domain "foo" %}
     <twig:Icon name="save" /> {{ 'foo.action2'|trans }} <twig:Icon name="caret-down" />
   </twig:Button>
   <twig:Button purpose="primary">
     {% trans_default_domain "foo" %}
     <twig:Icon name="eye" /> {{ 'foo.action3'|trans }}
   </twig:Button>
   <twig:Button purpose="info">
     {% trans_default_domain "foo" %}
     {{ 'foo.action4'|trans }}
   </twig:Button>
</twig:Card>

While the latter is more verbose, it's way more flexible and maybe you need only one special Button, that is again somewhat different, now you need to add an option for that as well even tho you only need it for one usage of the Button Component, while it would be eay to just put other content into it instead.

And here we are still talking about a simple Component, image the Card, that should have optional CardHeader & CardFooter and CardHeader and CardFooter should have ActionBar which has Actions:

<twig:Card>
  {% trans_default_domain "foo" %}
  <twig:CardHeader>
    Some Title Text, this could be moved to an title Attribute for sure
    <twig:block name="action_bar">
      <twig:ActionBar>
        {% trans_default_domain "foo" %}
        <twig:Action type="close" text-for-sr-only="{{ 'foo.action1'|trans }}" />
        <twig:Action type="collapse" text-for-sr-only="{{ 'foo.action2'|trans }} />
      </twig:ActionBar>
    </twig:block>
  </twig:CardHeader>
  <div>{{ 'foo.card_body_text'|trans }}</div>
  <twig:CardFooter>
    {% trans_default_domain "foo" %}
    <div>Some other markup that includes more translation keys in foo domain</div>
    <twig:block name="action_bar">
      <twig:ActionBar>
        {% trans_default_domain "foo" %}
        <twig:Action type="close" text-for-sr-only="{{ 'foo.action1'|trans }}" />
        <twig:Action type="collapse" text-for-sr-only="{{ 'foo.action2'|trans }} />
      </twig:ActionBar>
    </twig:block>
  </twig:CardFooter>
</twig:Card>

It becomes pretty wild 😅 But if you look at typical UI Framworks like Bootstrap, they have those Card Options, it becomes even more fun when you want to add handling for an image inside of that card, since you have no way to get the sequence of the inner content, like is image first element, then don't apply padding top etc. I mean you can work with css :has() and such, but oh boy

Kocal commented 3 months ago

I can understand the easy to use trans_default_domain only once, but as a workaround, what about specifying domain explicitly in each trans() call?

smnandre commented 3 months ago

@CMH-Benny oh i totally understand your need, was just illustrating twig component with no embed can be sometimes a good solution :)

What i want to insist on is: tthe trans_default_domain works internally like this.. and this is not a Twig component thing here :/

If you used include, or any Twig layout function or tag, it would be the same problem. And the solution could only be done on the symfony/symfony side

That's the feature i was suggesting you open an issue for on the symfony repo: "keep the domain in included / embed blocks".

But we really cannot do anything here as it's a Twig / Symfony code :)

--

More generally, there is something here that we need to answer and i agree with you. The problem is: currently Twig components are trying to do two things at the same time (isolated component with layout features / HTML component with global context), and this raises some frustrations on both sides.

CMH-Benny commented 3 months ago

Sorry for the late reply

I can understand the easy to use trans_default_domain only once, but as a workaround, what about specifying domain explicitly in each trans() call?

@Kocal That indeed would be an option, at least it reads quite better and way more clear. Usually we do that switch globally in the template because it's always the same in this context, but since Components live in their own contexts, this maybe is the best solution for now, thanks for that idea 👍

oh i totally understand your need, was just illustrating twig component with no embed can be sometimes a good solution :)

@smnandre All good, I totally understand that, as you already summed up, there is a more general issue lying here, that's why situations like this appear to be frustrating.

That's the feature i was suggesting you open an issue for on the symfony repo: "keep the domain in included / embed blocks".

Yeah, but at the time I don't really think that makes sense generally, so maybe it needs a second option like trans_override_domain that explicitly overwrites it from the position it appears down to every nested context and only stops on another trans_override_domain or if it finds a trans_default_domain again. Not sure if that is possible, tho :D

More generally, there is something here that we need to answer and i agree with you. The problem is: currently Twig components are trying to do two things at the same time (isolated component with layout features / HTML component with global context), and this raises some frustrations on both sides.

I agree 100%, it totally makes sense to think about that and introduce 2 types of components for each of those 2 purposes, so that I can exactly tell, this is a global context component like a button, that i want to use everywhere also inside of isolated components so they kinda merge the global kontext with the isolated context provided by the outer component. The vise versa is probably more easy, because an isloated component in a global context component won't interfere much within the context.

Thank you so much