-
Notifications
You must be signed in to change notification settings - Fork 82
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
WIP: Allow to pass a proc as fallback for conditional fallbacks #328
Conversation
Thanks very much for the PR! I have to think about this a bit, but also there seems to be a test failing with a "stack level too deep". This looks like it might not be related to the changes here. Could you try checking that the specs pass without the change? I'll try creating a build from master to see if it passes by default or if something changed in Rails. The fallbacks plugin code, btw, is quite confusing right now with |
Ok created #329. If it passes then the code here is breaking them. |
Take your time, looking for the best solution for this problem and you are the expert :-)
The PR is against
Yeah, I'd say this is a good one to clean up. Make it the same everywhere (I'd vote for singular version) and accept the same options with the same behavior for everything but |
Hmm ok well #329 passes. The PR should be against master, but 0-8-stable should be passing too. |
Yeah, it is on the ToDo list to have it on |
Ah that's right I started making many changes toward v1.0 on master, which explains why things were maybe breaking for you. I'll check if tests are passing without changes on 0-8-stable. |
Indeed specs are failing on 0-8-stable, I'll look into it... |
Yeah, though here more specs are failing ... just updated the branch to see if this fixes those spec failures with a subtle logic change. |
7e57576
to
24f06ec
Compare
I think I fixed the spec errors by refactoring the code a little bit. In addition I added some specs for the new functionality. This should pass on I can port this to |
129f087
to
59fffff
Compare
Yeah, now all old spec pass and I like the latest commit sha 59fffff ;-) |
303d2b5
to
e86da2c
Compare
59fffff
to
06cd86c
Compare
@shioyama any thoughts on this in the meantime? Is this something for 1.x, or do you have an idea how to implement this differently? Or not a feature you want to have in here? |
@doits Thanks very much, I see specs are passing which is good, and I see that you've added considerable test coverage. However the change adds many lines to the plugin... so I'm a bit hesitant. I agree that the feature itself would probably be attractive to many people, but ideally I'd like to see if we could simplify the implementation further. I need a bit of time to look at the code carefully, unfortunately I don't really have that time now and priority is on #323 right now. I'll see if I can make some suggestions inline in the meantime so maybe we can improve the PR. But again, thanks very much for the work and thought, it's a good idea and I'm not against adding it if we can find a way to make it a bit simpler, or more encapsulated perhaps. |
I should add that the requirements for the fallbacks plugin are already too convoluted, which is why you had to write all those ugly conditionals to handle things like To do that, I think the right place to start would be to ask what is a reasonable expectation for a fallbacks API. The current plugin was kind of patched together from APIs of similar gems (Globalize, etc.) and from user requests ( |
Also, although perhaps tests were failing there, I'd prefer to have this as a PR on the |
That's what I wanted to know, good to hear it 👍
Yes, I feel like this, too.
#323 is what bites me currently, too, so go on with that one first.
I can try to think of a good API with the same functionality for Then this feature could be added in a separate MR on top of the new API, which is probably much simpler then. So the plan would be to:
I can start with 1. if you like to, by first thinking of a new api and describing it (before implementing it). Of course if you already thought about it or have some expectations, I'd like to know them. |
@doits Very sorry for taking a while to reply, 1. sounds good if you want to make that PR. |
👍 Just that you know I'm currently really busy but still have this on my todo list ... might take some time for me though until I can think about this again. |
Time passes by and ... I just saw you released 1.0.0 🎉 congrats, great work @shioyama! And with the Edit: See #328 (comment) for an updated version. module FallbackProcPlugin
extend ::Mobility::Plugin
requires :backend, include: :before
included_hook do |_, backend_class|
backend_class.include BackendReader.new(options[:fallback_proc])
end
class BackendReader < Module
def initialize(fallback_proc)
define_method :read do |locale, **options|
value = super(locale, **options)
return value if !fallback_proc || value.present?
fallback_locale = model.instance_exec(&fallback_proc)
return value if fallback_locale.blank?
super(fallback_locale, **options)
end
end
end
::Mobility::Plugins.register_plugin(:fallback_proc, FallbackProcPlugin)
end
Mobility.configure do
plugins do
fallback_proc
end
end
class SomeModel
extend Mobility
attribute :main_locale
translates :title, fallback_proc: proc {
# return any locale you want from here
# fallback to `main_locale` attribute of current model
main_locale
}
end If you think this is something for Otherwise I am happy I could write my own plugin and it works so easily :-) Edit: Setting a default fallback proc is also possible but requires some work around by defining the proc before as a variable: Mobility.configure do
default_fallback_proc = proc { ... }
plugins do
fallback_proc default_fallback_proc
end
end Without the variable declaration above |
Thanks @doits ! Great that you've already picked up the new plugin format 👍 Now that 1.0 has been released I'd be interested in improving the original fallbacks plugin to include this feature and also simplify the other existing options. We could then to aim to release that change in a v1.1. Let me think about this a bit more but if anyone else is following has thoughts on what a "fallbacks" plugin should have in it please share here 😄 |
Maybe having multiple simple fallback plugins is the way to go? Something like:
This could keep each plugin simple, but of course users would then need to choose which one is the right for them. |
@doits A few things quickly:
Regarding the last point, I simplified things slightly by removing I'm going to create a quick branch to see what this would look like on master. |
I think first thing to clarify is usage (regardless of implementation). I was imagining that you would pass a proc which would take a locale, and return the fallback locale. Maybe the If anyone else is following, what would be the most natural thing in this case? |
Yeah, the locale probably should be passed, too, in case the fallback depends on the original locale.
I think this is reasonable. Since the idea is to have a fallback specific for an instance it makes sense to make accessing the instance as easy as possible. For reference, this is what I am using in my app for many months without problems already (though the original locale is not passed to the proc here yet, but this is a simple addition): # last updated 2021-10-04
# Fallback locale is returned by some attribute or method of the instance
#
# To use the locale in column `main_locale` as fallback locale:
# translates :title, model_attribute_fallback: :main_locale
#
# In this example `main_locale` can be a model attribute or even a *method* with some custom logic
module ModelAttributeFallbackPlugin
extend ::Mobility::Plugin
requires :backend, include: :before
included_hook do |_, backend_class|
next unless options[:model_attribute_fallback]
backend_class.include BackendReader.new(options[:model_attribute_fallback])
end
class BackendReader < Module
def initialize(fallback_attribute)
define_method :read do |locale, **options|
value = super(locale, **options)
if value.present? ||
options[:locale] ||
options[:fallback] == false ||
options[:model_attribute_fallback] == false
return value
end
fallback_locale = model.public_send(fallback_attribute)
return value if fallback_locale.blank? || fallback_locale == locale
super(fallback_locale, fallback: false, **options)
end
end
end
::Mobility::Plugins.register_plugin(:model_attribute_fallback, ModelAttributeFallbackPlugin)
end
# Fallback locale is returned by a proc, `self` is the model instance when proc is called
#
# To use:
# translates :title, fallback_proc: proc { ...custom_logic, `self` is the model instance... }
module FallbackProcPlugin
extend ::Mobility::Plugin
requires :backend, include: :before
included_hook do |_, backend_class|
next unless options[:fallback_proc]
backend_class.include BackendReader.new(options[:fallback_proc])
end
class BackendReader < Module
def initialize(fallback_proc)
define_method :read do |locale, **options|
value = super(locale, **options)
if value.present? ||
options[:locale] ||
options[:fallback] == false ||
options[:fallback_proc] == false
return value
end
fallback_locale = model.instance_exec(&fallback_proc)
return value if fallback_locale.blank? || fallback_locale == locale
super(fallback_locale, fallback: false, **options)
end
end
end
::Mobility::Plugins.register_plugin(:fallback_proc, FallbackProcPlugin)
end |
@doits Really glad you're making use of the plugin framework. I do feel though that there's some unneeded complexity, and I'd like to make it easier to do what you're doing (e.g. without having to use #538 should do that. Your example above could then be simplified given the fact that module ModelAttributeFallbackPlugin
extend ::Mobility::Plugin
requires :backend, include: :before
module BackendMethods
def read(locale, **kwargs)
return super unless (fallback_attribute = options[:model_attribute_fallback])
value = super
if value.present? ||
kwargs[:locale] ||
kwargs[:fallback] == false ||
kwargs[:model_attribute_fallback] == false
return value
end
fallback_locale = model.public_send(fallback_attribute)
return value if fallback_locale.blank? || fallback_locale == locale
super(fallback_locale, fallback: false, **kwargs)
end
end
::Mobility::Plugins.register_plugin(:model_attribute_fallback, ModelAttributeFallbackPlugin)
end
module FallbackProcPlugin
extend ::Mobility::Plugin
requires :backend, include: :before
module BackendMethods
def read(locale, **kwargs)
return super unless (fallback_proc = options[:fallback_proc])
value = super
if value.present? ||
kwargs[:locale] ||
kwargs[:fallback] == false ||
kwargs[:fallback_proc] == false
return value
end
fallback_locale = model.instance_exec(&fallback_proc)
return value if fallback_locale.blank? || fallback_locale == locale
super(fallback_locale, fallback: false, **kwargs)
end
end
::Mobility::Plugins.register_plugin(:fallback_proc, FallbackProcPlugin)
end I'm not following the context much here and haven't actually tested the changes, just modified them to demonstrate how things can be simplified. Basically you shouldn't need a module builder or an Notice I'm distinguishing |
I'm trying to make fallbacks more dynamic (which is in the same vein as #314 for reference). There I did a
default
workaround to make this possible, but I think it should be implemented directly in fallbacks module. To recap:I have a
User
model and each user can select his ownmain_locale
which should be used as a fallback for all translations.This means that fallbacks are different per instance and not application or class wide anymore. To make this possible I updated the fallbacks plugin to allow a
proc
as a option:The proc gets passed the model as a first argument.No arguments get passed at the moment, not sure if anything should be passed from the plugin.
ToDo:
0-8-stable
master
0-8-stable
master
Another possible solution I can think of is to pass a database column or sql query to fetch the locale.
proc
and requires to have fallbacks saved in the database exactly like they should be used (maybe this is a pro?)What do you generally say about this idea? Is this something you'd want to have directly in the fallbacks plugin? Do you see any side effects?
If this looks reasonable to integrate I can add the specs and refactor it it.