holidays / holidays

A collection of Ruby methods to deal with statutory and other holidays. You deserve a holiday!

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Making holiday names referenceable

joallard opened this issue · comments

As a resident of a bilingual country (Canada), I need to display holidays for my users in the language of their choice. I realize it makes sense, for instance if I look at France, to just have the names in the language of the country. I don't think it'd be that useful for instance, to have France's holidays translated in English (or else). But for some multilingual jurisdictions, it's a little needed.

Would you eventually accept a PR, after some discussion about how this should be implemented?

As a workaround, we could probably use the name string to use in a translation dictionary. However, if the name changes even cosmetically, we need to change the translation key. It makes it very brittle.

What if, in the definitions for some regions, we had another key symbol datapoint, eg. :christmas for Christmas Day, or :patriotes for Patriotes Day in :ca_qc, and so on? That'd be a good first step; we could then see if we want first-class I18n support but still retrocompatible since adding those keys is quite the endeavor.

In addition, it would make storage in general easier, not only translations. If I add in one of my models "we're closed on Boxing Day", I probably want to be able to have some sort of record saying {holiday: "ca/boxing_day", closed: true}, instead of indexing it with "Boxing Day".

What would probably make sense is to parameterize holiday names, and prefix them with their "region file name", and then make keys unique to that scope. We'd then be able to do Holidays.find("ca/boxing_day")

I'll share with you the code that is currently needed to do this:

  def find_holiday_for(datetime)
    possible_hols = Holidays.on(datetime.to_date, list_holiday_regions)
    return nil unless possible_hols && !possible_hols.empty?

    thatday = Set.new

    possible_hols.each do |hol|
      hol[:regions].each do |region|
        thatday << normalize_holiday_name(region, hol[:name])
      end
    end

    targeted = @holidays.map(&:holiday).to_set

    result = thatday & targeted

    return nil if result.empty?

    result.first
  end

  def list_holiday_regions
    @holidays.
      map(&:holiday).
      map{|s| s.split('/')[0] }.
      uniq.map(&:to_sym)
  end

  def normalize_holiday_name(region, name)
    name = ActiveSupport::Inflector.transliterate(name)
    name = name.parameterize(separator: ?_)
    name = "#{region}/#{name}"
  end

Hello @joallard, sorry for the radio silence, I have been sick this week so I'm behind on almost all of my obligations.

This is a very good suggestion! To help me understand a bit better, could you provide some usage examples? I'm having a bit of trouble understanding how the lookups would happen from the user's perspective.

The reason I ask for more clarity is that I've had i18n stuff on my mind for quite a while now. It originally came to my attention with the Belgium (https://github.com/holidays/definitions/blob/master/be_fr.yaml and https://github.com/holidays/definitions/blob/master/be_nl.yaml). The solution to have two non-ISO regions (for be_fr and be_nl) is not great. I did it as a stopgap.

Based on the be example I was initially thinking about something like what I have below. It's much more driven by individual definitions:

months:
  0:
  - name: Pasen
      language:
        fr: Pâques
        nl: Pasen
    regions: [be]
    function: easter(year)

The first one is the default (which....might require some compromise. Not sure how I want to handle that) and then underneath it we could have as many languages as we wanted. For the default we can also just not have one, requiring that someone specify a language in those cases. That might be better when there is no single 'right' or 'dominant' language.

We would need to add in an option so that when looking up the holiday you would specify the language you wanted it to return with:

Holidays.on(Date.civil(2016, 1, 1), :be, :language => :fr)

But I only wrote out the above to show you what I was thinking. I will readily admit that this might be a bit fragile. I like your suggestion to have a more common i18n setup that can be used. Initially I was simply thinking that since most regions/holidays wouldn't need it that I could keep things very simple. My suggestion above would only require changing holidays that need this functionality.

All suggestions are welcome. If you have thoughts/feedback on my proposal I am happy to hear it.

@ttwo32 I would also like your feedback if you have time.

No worries @ptrimble, that's actually still a good turnaround!

Okay, so the way I filed this, there are two separate concerns here:

(1) is to be able to reference a holiday in the book using a symbol (ie. in code parlance). So for example I want to be able to store be/easter in my DB if my user has indicated they take a vacation on Easter. I don't want the DB to care about the pretty-name Pâques.

(2), and it kind of goes with it, I want to be able to use that symbol to then render the name in a sensible language. Not saying we'll be able to translate Pâques to whatever it is in Japanese, but as a Dutch-speaking Belgian, I want to be able to read "Pasen" in the system.

Additionally, you point out that at best, we disrupt the current API the least. Let's see what we can do.

The current invocations, for reference:

[156] pry(main)> Holidays.on(Date.civil(2016, 1, 1), :be)
Holidays::InvalidRegion: Holidays::InvalidRegion
from /Users/jon/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/holidays-5.0.0/lib/holidays/finder/context/parse_options.rb:74:in `block in validate!'
[157] pry(main)> Holidays.on(Date.civil(2016, 1, 1), :be_fr)
=> [{:date=>Fri, 01 Jan 2016, :name=>"Jour de l'an", :regions=>[:be_fr]}]
[158] pry(main)> Holidays.on(Date.civil(2016, 1, 1), :ca)
=> [{:date=>Fri, 01 Jan 2016, :name=>"New Year's Day", :regions=>[:ca]}]

(Thinking out loud as well here.) Without that constraint, what makes sense for the underlying data is to have a symbol, and for the name to be renderable as a string:

months:
  0:
    - key: easter # or be/easter or be/paques
      name:
        fr: Pâques
        nl: Pasen
      regions: [be]
      function: easter(year)

Our name field can now either be a string or a locale map. (Oh, I just noticed the return values are not Holiday objects.)

Ideally what I would've seen is: query a list of holidays. Call holiday.name you get the holiday name in the default language. Call holiday.name(:fr) and you get it in that language. Hopefully that would work with I18n.t. That would be a little backward-breaky.

Maybe we could add an option to the .on() query to fetch a list of objects rather than hashes? Internally we'd use that object representation, but legacy call would get hashes. Or we could already give holiday objects, and make them accessible with the legacy call holiday[:name] as well as holiday.name.

Adding the translations to the definition file is not the ideal way. You should rely on the i18n system to do this. That way if you include the default naming in the gem, I can always change it in my i18n yml file.

@ptrimble I like to have a more common i18n and support multi language.
Thinking About multilange definition(ex.Belgium ), Ideally they should be included one file.
Because that is simple.
Aside translation problem, it's good idea that we make holiday name referenceable.

I hear the feedback being given and I understand why having the translations in the definitions is problematic. Let me think about this for a bit and come back with another concrete responses based on what we have said.

Side note: my wife is due to deliver our first kid on December 16th so things are a bit nuts for me at the moment. I'm not sure how my schedule will look in the coming weeks so I apologize in advance for any delays! 😄

Any news or progress about this? :D

Our use case is we want to provide the user with a list of holidays to add to their paid holiday list. and as such we want our definition to simply be recurring holiday == us/independence_day and then in our backend code we would create the "instance every year.. thus asking the lib "what date is us/independence_day for 2019". and create the DB record (we have to do this as we have metadata that is attached per date. e.g. # of hours paid, who has been paid this year, etc..)