-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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
Set fallback locale for Mobility based on current store #11906
Conversation
Changes preview: |
fallbacks = store.supported_locales_list.each_with_object({}) do |locale, object| | ||
object[locale] = [store_default_locale] | ||
end |
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.
You can also consider doing:
fallbacks = store.supported_locales_list.each_with_object({}) do |locale, object| | |
object[locale] = [store_default_locale] | |
end | |
fallbacks = store.supported_locales_list.to_h { |locale| [locale, [store_default_locale]] } |
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.
Hm, isn't it more explicit and clear when we do each_with_object
?
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.
I don't have a strong preference for either to be honest. each_with_object
probably looks more familiar to most readers. In defense of #to_h
- to me it's clear in the way it just deals with simple [key, value]
objects instead of having this "state" object that's filled in a loop. I like the original as well though, hence my original comment mentioning the suggestion as something to consider.
fallbacks_class = I18n::Locale::Fallbacks | ||
fallbacks_instance = fallbacks_class.new(fallbacks) |
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.
Any reason why it's not just:
fallbacks_class = I18n::Locale::Fallbacks | |
fallbacks_instance = fallbacks_class.new(fallbacks) | |
fallbacks_instance = I18n::Locale::Fallbacks.new(fallbacks) |
Or even doing the entire thing in one line:
Mobility.store_based_fallbacks.value = I18n::Locale::Fallbacks.new(fallbacks)
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.
That were some leftovers from the original implementation in mobility. Good point :)
def read(locale, fallback: true, **kwargs) | ||
return super(locale, **kwargs) if !fallback || kwargs[:locale] | ||
|
||
locales = fallback == true ? Mobility.store_based_fallbacks.value[locale] : [locale, *fallback] |
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.
There's already a !fallback
check above so you know for sure that it's true
here. locales
will always be equal to:
Mobility.store_based_fallbacks.value[locale]
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.
That's a leftover from the stock fallbacks implementation, good point :)
return value if Util.present?(value) | ||
end | ||
|
||
super(locale, **kwargs) |
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.
I don't get this part. The method gets a locale
positional argument, but also a locale
keyword argument. Shouldn't the logic be something in the lines of:
- Check whether a translation exists for
locale
- If it exists - return it.
- If it doesn't exist then try to use fallback.
And here I see that the fallback is calculated first, and only then the original locale in the super(locale, **kwargs)
line. The first line that short-circuits if the kwarg is set is also confusing. What's the difference between the keyword argument and the positional argument? I guess it's some Mobility internal so in our codebase it looks a bit muddy.
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.
@kshalot that's what this implementation does.
The logic is as follows:
- If a specific locale is requested or fallback is disabled - skip the fallbacks mechanism and read the value without the fallbacks in specified locale
- Iterate through the fallbacks for requested locale. This list always starts with the locale itself, so e.g. fallbacks['en'] = ['en', (other locales that are configured)]. If a value is found for any of them, return it
- If this didn't return any result, read the value without the fallbacks for specified locale
The signature of this function is not the most clear one, but I'd prefer to keep it, not to break other mobility plugins that need to work alongside it
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.
Okay. The signature is totally confusing, but I see that the original fallbacks work in a similar way. By glancing at the code, I don't see where the locale
kwarg is used. Are we sure it's not a bug?
To illustrate my point using the first step of your answer - "If a specific locale is requested or fallback is disabled...". Why is the bold part not checked with locale
? What is the difference between that and kwargs[:locale]
? If kwargs[:locale]
is the specific locale that was requested then what is locale
? I guess if it did not work then someone would have caught it by now, but it's very muddy. Do you have some examples of what the values of those variables are for some call?
I agree that mirroring the function signature of the original fallback plugin is a smart move, but I just want to make sure we don't follow it blindly given the fact that the original code contained some dubious statements.
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.
I did some further investigation on that, it seems that Mobility's internals call it like read(locale, locale: true)
. Example here: https://github.com/shioyama/mobility/blob/master/lib/mobility/plugins/active_model/dirty.rb
So the kwargs[:locale] can be true/false and it has a different semantic.
This part of the implementation is a slightly modified implementation of the default fallbacks plugin, so I kept the behavior in this case the same.
fallbacks_class = I18n::Locale::Fallbacks | ||
fallbacks_instance = fallbacks_class.new(fallbacks) | ||
|
||
Mobility.store_based_fallbacks.value = fallbacks_instance |
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.
Maybe we could abstract this into Mobility in some way? I mean not explicitly set the instance variable on the Mobility
singleton class. My problem with this now is that it leaks the way we store the fallback in a way. Doing a
Mobility.set_store_fallback(fallbacks_instance)
would intuitively make more sense to me, since no implementation details are exposed.
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.
Hm, how about I write custom store_fallback
and store_fallback=
methods that hide this behavior?
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.
Yeah, a getter and setter would do the job.
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.
Please take a look again
aca59bc
to
e73465f
Compare
e73465f
to
e48b2e6
Compare
# Applies fallbacks plugin to attributes. Completely disables fallbacks | ||
# on model if option is +false+. | ||
included_hook do |_, backend_class| | ||
unless options[:fallbacks] == false |
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.
Maybe this is clearer? if x
instead if not not x
unless options[:fallbacks] == false | |
if options[:fallbacks] |
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.
It would be, but I'm not sure about the interface here :) After this change it will behave differently for a falsey value (e.g. nil), and I think Mobility's default fallbacks plugin would work if that option is not set. Only setting it explicitly to false disabled it.
return value if Util.present?(value) | ||
end | ||
|
||
super(locale, **kwargs) |
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.
Okay. The signature is totally confusing, but I see that the original fallbacks work in a similar way. By glancing at the code, I don't see where the locale
kwarg is used. Are we sure it's not a bug?
To illustrate my point using the first step of your answer - "If a specific locale is requested or fallback is disabled...". Why is the bold part not checked with locale
? What is the difference between that and kwargs[:locale]
? If kwargs[:locale]
is the specific locale that was requested then what is locale
? I guess if it did not work then someone would have caught it by now, but it's very muddy. Do you have some examples of what the values of those variables are for some call?
I agree that mirroring the function signature of the original fallback plugin is a smart move, but I just want to make sure we don't follow it blindly given the fact that the original code contained some dubious statements.
@@ -267,6 +267,7 @@ | |||
let(:translated_slug) { 'test_slug_es' } | |||
|
|||
before do | |||
Spree::Locales::SetFallbackLocaleForStore.new.call(store: store2) |
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.
Now that I'm looking at the tests - it would probably be wise to cover this fallback functionality with tests in general. I guess what we are missing is:
- Test whether the locale controller helper sets the fallback when
current_store
is present. - Test whether the fallback plugin itself works as intended.
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.
- This is covered e.g. by the requests tests for the API. I forgot to remove setting it manually here, it works without it. I moved away from generating the slugs with fallbacks manually, and it now fully relies on mobility, so the old assertions check this behavior end-to-end now.
- I added two specs for that
def call(store:) | ||
store_default_locale = store.default_locale | ||
fallbacks = store.supported_locales_list.each_with_object({}) do |locale, object| | ||
object[locale] = [store_default_locale] |
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.
What if there's only one locale? Will it still try to fallback using itself? This would obviously be redundant.
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.
It won't change anything then. I18n::Locale::Fallbacks consolidates duplicate entries in the list of fallbacks, and the first "fallback" on the list for each locale is the actual locale. Only the later entries denote actual fallbacks in case the requested one is not present.
e48b2e6
to
383ed45
Compare
383ed45
to
8a25054
Compare
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.
Just three remarks (one actual question in the spec).
Apart from that I'd say it looks good 👍 ✅
let(:name_en) { 'name en' } | ||
let(:name_pl) { 'name pl' } | ||
|
||
let!(:translation_en) { product.translations.find_or_create_by(locale: 'en') { |e| e.name = name_en } } |
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.
Is this translation (and the en locale in general) relevant to any of the tests?
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.
Or is it there to demonstrate that the fallback will be the default_locale
and not some random locale?
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.
We want to check whether it falls back to a specific locale (in this case pl
), without taking the en
value (which is by default set as I18n.default_locale and Mobility uses it as a default for fallbacks)
@@ -66,10 +67,12 @@ class Taxon < Spree::Base | |||
|
|||
def set_permalink | |||
parent = translated_model.parent | |||
name_with_fallback = name || translated_model.name |
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.
Just a question - this refers to the name
in the translation and when it's not set it falls back to the model's name
which kicks in the whole fallback mechanism, right?
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.
Exactly
supported_locales = store.supported_locales_list | ||
|
||
supported_locales.each_with_object({}) do |locale, hash| | ||
hash[locale] = localized_slugs[locale] || localized_slugs[default_locale] | ||
hash[locale] = I18n.with_locale(locale) { slug } |
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.
Technically, we could loosen the contract between this module and the including module by introducing a dependency on FriendlyId
. I think you can access the slug_column
config value by doing model.friendly_id_config.slug_column
. Not great either, but it popped in my mind after seeing that you had to add a slug
alias to the Taxon model just so this works. Something to consider in the future maybe
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.
We wanted to unify the naming of the permalink
-> slug
column anyway, as it's a bit confusing right now. Technically all of them should be called slug
, as permalink
refers to the whole URL, including domain name.
Fixes #11899