Skip to content

Sidecar translations#552

Closed
elia wants to merge 1 commit intoViewComponent:mainfrom
elia:sidecar-i18n
Closed

Sidecar translations#552
elia wants to merge 1 commit intoViewComponent:mainfrom
elia:sidecar-i18n

Conversation

@elia
Copy link
Contributor

@elia elia commented Dec 11, 2020

Summary

Having sidecar translations is mentioned in multiple places in the project and the two apps using components I'm aware of are both doing it. Locally we solved with a less refined version of the attached code.

Other Information

This builds on the work started in #74.

Expects a component to have a file with the same basename and the .yml extension, holding translations scoped to the component's path.

A component YML should look like this:

  en:
    hello: "Hello World!"

And inside the component will be possible to call

<%=  t(".hello") %>

Internally the scope will be expanded using the template's path.
As an example, the translation above from within app/components/greeting/component.html.erb will result in an internal I18n key like this:

  en:
    greeting:
      component:
        hello: "Hello World!"

This PR is sponsored by @nebulab

@elia elia force-pushed the sidecar-i18n branch 5 times, most recently from 58a5c9a to e53b398 Compare December 11, 2020 18:56
@elia elia marked this pull request as ready for review December 11, 2020 18:59
@elia
Copy link
Contributor Author

elia commented Dec 11, 2020

For some reason Rails 5.0.0 on Ruby 2.7 was failing with a weird bundler error, updating to the latest patch seems to have fixed the problem.

@joelhawksley
Copy link
Member

@elia thank you for the PR! We should be able to review it early next week.

In the meantime, I believe we can revert the dependency version changes. We see that build failure from time to time, but haven't been able to identify the cause. Rebuilding usually fixes it.

@elia
Copy link
Contributor Author

elia commented Dec 12, 2020

@elia thank you for the PR! We should be able to review it early next week.

Awesome!

In the meantime, I believe we can revert the dependency version changes. We see that build failure from time to time, but haven't been able to identify the cause. Rebuilding usually fixes it.

Done ✅

Do you want me to send that change into another PR? Staying up-to-date seems a good way to avoid needless headaches.

# hello: "Hello World!"
#
#
def self.load(components_root: "#{Rails.root}/app/components", glob: "**/*component.{yml,yaml}")
Copy link
Contributor

Choose a reason for hiding this comment

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

I really love this change, but I think it'd be great if we could make this part of the compilation step.

I think that'd let us more tightly couple components to translation files, removing the need to configure translations. I think it might also help with template reloading, but that may need more investigation.

Copy link
Contributor

Choose a reason for hiding this comment

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

Building on that thought a bit, it'd be great if we could use the component's file name to determine the translation file name instead of requiring the translation files to end in component.*.

e.g. If I have modal.rb, I'd expect the translation file to be called modal.yml, or modal/modal.yml (or .yaml as an extension).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea of using the compilation step to add paths to I18n is really neat, and source_location is perfect to do that.

Also about reloading I missed an initializer during the extraction from our application, which would be integrated in engine.rb (it's modeled after the original I18n reloading code):

# Reload component translations whenever we change them.
Rails.application.configure do
  config.after_initialize do |app|
    app.config.file_watcher.new [], "#{Rails.root}/app/components" => ".yml" do
      I18n.reload!
    end.tap do |reloader|
      app.reloaders << reloader
      app.reloader.to_run do
        reloader.execute_if_updated { require_unload_lock! }
      end
    end
  end
end

With an hypothetical modal.rb component it should be already possible to adjust the glob, even to a generic **/*.y{a,}ml, to encompass any YAML in the components folder.


So, before proceeding with the implementation inside compile, I'd like to have your opinion on a couple of considerations:

  • I foresee possible headaches for applications not being strict about only using the translations within the component
  • We might still need the reloader to properly pickup changes (renames/deletions)

The code would go more or less like this:

def compile
  unless I18n.paths.include? component_locale
    I18n.paths << component_locale
    I18n.reload!
  end
  # ...
end

Copy link
Contributor

Choose a reason for hiding this comment

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

I foresee possible headaches for applications not being strict about only using the translations within the component

Can you share a bit more about that concern? What's an example of "not being strict"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can you share a bit more about that concern? What's an example of "not being strict"?

Sure!

What I mean is that translations are expected to be there globally, but if we load them only when the component is compiled they might not be there in every environment/situation (think background jobs, rake tasks, partials, etc.) unless we ensure components are all eagerly compiled all the time.

The general point is that translations are expected to be globally available and this change would potentially brake this expectation. On the other and once loaded are exactly globally available like every other translation.

Maybe a more acceptable alternative would be to have tiny I18n backends local to the component, and able to fallback to the global translations. That might be heavier in terms of performance/memory/allocations but at least would be consistent.

Copy link
Contributor

Choose a reason for hiding this comment

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

What I mean is that translations are expected to be there globally, but if we load them only when the component is compiled they might not be there in every environment/situation (think background jobs, rake tasks, partials, etc.) unless we ensure components are all eagerly compiled all the time.

Translations being available globally breaks the implicit and explicit encapsulation that view component assumes. For example, if we have two components with the same name they might have an i18n naming collision. I think it's important to handle that edge case.

Copy link
Contributor

@BlakeWilliams BlakeWilliams left a comment

Choose a reason for hiding this comment

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

Thanks for taking this on! This is a really great feature.

@elia elia requested a review from BlakeWilliams December 16, 2020 21:50
Base automatically changed from master to main December 21, 2020 21:44
component_translations = YAML.load_file path
scopes = relative_path.sub(/\.ya?ml/, "").split("/")

component_translations.to_h.each do |locale, scoped_translations|
Copy link

@cj cj Dec 28, 2020

Choose a reason for hiding this comment

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

I have done some testing, and to allow options like:

en:
  greeting:
    hello: "Hello World!"

# AND

en:
  greeting:
    component:
      hello: "Hello World!"

Doing:

        component_translations.to_h.each do |locale, scoped_translations|
          translations[locale] ||= {}
          translations[locale].deep_merge!(scoped_translations)
        end

<%= t('greeting.hello') %>

seems to work better. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that a component would always use the relative notation, if that's the case the component's naming shouldn't matter, e.g. both these scenarios are expected to work:


app/components/greeting.html.erb

<%= t(".hello") %>

app/components/greeting.yml

en:
  hello: "Hello World!"

app/components/greeting/component.html.erb

<%= t(".hello") %>

app/components/greeting/component.yml

en:
  hello: "Hello World!"

Am I missing some context with regard to this?

@elia elia force-pushed the sidecar-i18n branch 2 times, most recently from 740e1e5 to 559e7eb Compare January 31, 2021 21:47
@Spone Spone self-requested a review February 15, 2021 17:09
Comment on lines +24 to +28
# en:
# greeting:
# component:
# hello: "Hello World!"
Copy link
Collaborator

@Spone Spone Feb 22, 2021

Choose a reason for hiding this comment

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

I'm curious about the rationale behind this naming/namespacing.

By default the naming of the template file for an ExampleComponent with sidecar would be: app/components/example_component/example_component.html.erb so I think the docs should show this case instead.

What would the structure of the internal I18n key be in this case? en.example_component.example_component.hello, right?

Copy link
Collaborator

Choose a reason for hiding this comment

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

👋 @elia did you see this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I did read it but then somehow skipped it when replying

I'll update the doc, the use of example/component.* was just coming from the way we organized them in the app I extracted this from.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't worry about it ;)

I find the en.example_component.example_component.hello I18n key not ideal, and I'm wondering if it shouldn't be something like en.view_component.example_component.hello or some other naming that:

  • uses a view_component or components namespace to prevent collision with other parts of the app
  • removes the component name duplication
  • is consistent whether you have:
    • a regular component (with a template placed in app/components/example_component.html.erb)
    • a sidecar component (with a template placed in app/components/example_component/example_component.html.erb)
    • an inline component (so the I18n.t method is called from the component call method)

What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the idea of both the prefix and collapsing the sidecar components path with the duplicate name, the only thing I want to point out before trying to implement them is that we need to keep this configuration in synch with virtual_path in ViewComponent::Base because that's the one that will drive relative translations inside templates.

So, probably we should start moving some of this stuff to engine-level configurations, including maybe the components root and the glob for the YAML files. That would be ok?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to request @joelhawksley and @BlakeWilliams opinion about this, as I'm not sure what's the best move.

Copy link
Contributor

@BlakeWilliams BlakeWilliams Mar 3, 2021

Choose a reason for hiding this comment

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

Building on #552 (comment), if we go down that path I think we could avoid coupling i18n key names to specific components because each component will have access to the global i81n, its sidecar i18n, but not the sidecar i18n of other components.

So, probably we should start moving some of this stuff to engine-level configurations, including maybe the components root and the glob for the YAML files. That would be ok?

We should avoid configuration wherever possible (or at least have some great defaults), so if we can make i18n work out of the box that would be preferable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@BlakeWilliams please checkout #660 which features a version of sidecar i18n as you intended, the global one is still available via I18n.t or helpers.t.

@elia elia requested review from Spone and jonspalmer February 26, 2021 15:29
@elia elia force-pushed the sidecar-i18n branch 6 times, most recently from 4691f9e to 54b0db5 Compare February 26, 2021 22:48
@elia
Copy link
Contributor Author

elia commented Feb 27, 2021

The CI is having random failures with bundler (v1) and Browser did not produce websocket url within 5 seconds (Ferrum::ProcessTimeoutError) in the PVC job, I don't have the permission to restart the build, let me know if I should just keep trying using fake force-pushes.

@Spone
Copy link
Collaborator

Spone commented Feb 28, 2021

The CI is having random failures with bundler (v1) and Browser did not produce websocket url within 5 seconds (Ferrum::ProcessTimeoutError) in the PVC job, I don't have the permission to restart the build, let me know if I should just keep trying using fake force-pushes.

I retried it a couple times, CI / pvc (pull_request) is failing every time. Not sure what to do @joelhawksley?

@joelhawksley
Copy link
Member

The CI is having random failures with bundler (v1) and Browser did not produce websocket url within 5 seconds (Ferrum::ProcessTimeoutError) in the PVC job, I don't have the permission to restart the build, let me know if I should just keep trying using fake force-pushes.

I retried it a couple times, CI / pvc (pull_request) is failing every time. Not sure what to do @joelhawksley?

We've marked that build as optional while we investigate ❤️

Expects a component to have a file with the same basename and
the .yml extension, holding translations scoped to the component's path.

A component YML should look like this:

  en:
    hello: "Hello World!"

And inside the component will be possible to call

  t(".hello") # => "Hello World!"

Internally the scope will be expanded using the template's path.
As an example, the translation above from within `app/components/example/component.html.erb`
will result in an internal I18n key like this:

  en:
    example:
      component:
        hello: "Hello World!"
@ViewComponent ViewComponent deleted a comment from patchwork-her Mar 3, 2021
@ViewComponent ViewComponent deleted a comment from patchwork-her Mar 3, 2021
@elia elia mentioned this pull request Mar 8, 2021
@elia
Copy link
Contributor Author

elia commented Mar 16, 2021

This has been replaced by #660 in which the I18n lookup will only include the sidecar translations instead of having them mixed with the global Rails I18n.

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.

6 participants