Conversation
58a5c9a to
e53b398
Compare
|
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. |
|
@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. |
Awesome!
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}") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
endWith 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
# ...
endThere was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
BlakeWilliams
left a comment
There was a problem hiding this comment.
Thanks for taking this on! This is a really great feature.
| component_translations = YAML.load_file path | ||
| scopes = relative_path.sub(/\.ya?ml/, "").split("/") | ||
|
|
||
| component_translations.to_h.each do |locale, scoped_translations| |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
740e1e5 to
559e7eb
Compare
| # en: | ||
| # greeting: | ||
| # component: | ||
| # hello: "Hello World!" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_componentorcomponentsnamespace 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.tmethod is called from the componentcallmethod)
- a regular component (with a template placed in
What do you think?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
I'd like to request @joelhawksley and @BlakeWilliams opinion about this, as I'm not sure what's the best move.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
4691f9e to
54b0db5
Compare
|
The CI is having random failures with bundler (v1) and |
I retried it a couple times, |
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!"
|
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. |
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
.ymlextension, holding translations scoped to the component's path.A component YML should look like this:
And inside the component will be possible to call
Internally the scope will be expanded using the template's path.
As an example, the translation above from within
app/components/greeting/component.html.erbwill result in an internalI18nkey like this:This PR is sponsored by @nebulab