Country flags in dropdown language menu

Hello, I would like some advice regarding the country flags in the dropdown menu. I found this tutorial which I hope is correct.
As a newbie programmer, I’ve never done anything like this and do not fully understand any part of code, so I don’t know if I’m on the right way.

So I have new helper with this:

defmodule CookieWeb.FlagHelpers do
  @flag_offset 127397
  def get_flag(country_code) when byte_size(country_code) == 2 do
    <<s1::utf8, s2::utf8>> = String.upcase(country_code)
    <<(s1 + @flag_offset)::utf8, (s2 + @flag_offset)::utf8>>
  end
  def get_flag(country_code), do: country_code
end

In cookie_web.ex I have:

import CookieWeb.FlagHelpers
      
def available_languages, do: Application.get_env(:cookie, :available_languages)
  |> Enum.map_join(&Flags.get_flag()/1)

available_languages I have define here in config.exs:

config :cookie,
  available_languages: ~w(en sk)a

Menu I have define in html.heex here:

         <div id="lang-menu-bar" class="hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
            <%= for lang <- available_languages() do %>
              <%= link Gettext.gettext(CookieWeb.Gettext, to_string(lang)), to: Routes.lang_path(@conn, :set, lang), class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 #{if lang == @current_user.default_lang, do: 'font-bold'}", role: "menuitem", tabindex: -1 %>
            <% end %>
          </div>

Screenshot 2022-08-27 at 18.27.23

Now I have this error:

My Controller is here:

defmodule CookieWeb.LangController do
  use CookieWeb, :controller

  def set(conn, %{"lang" => lang}) do
    Cookie.Accounts.update_user_default_lang(
      conn.assigns.current_user,
      String.to_existing_atom(lang)
    )

    case Plug.Conn.get_req_header(conn, "referer") do
      [referer] ->
        redirect(conn, external: referer)

      _ ->
        redirect(conn, to: "/")
    end
  end
end

If I’m on the right way, what do I need to do next to project the national flags into the menu.

Thank you for any help and answer! :slight_smile:

Your helper module is named as FlagHelpers while the error say you’re calling it as Flags.

Maybe check that and see where it leads you.

1 Like

Country flag emoji are not supported on windows (on their default font).

1 Like

I have still same error, but you are absolutely right! Thanks.

Ou… Thanks a lot. Please any idea how I can add flags to my menu to make it work for all types of systems?

The error is because there’s no module named Flags in your version, it’s been renamed to CookieWeb.FlagHelpers. Since it’s imported, you could refer to it without a module name:

def available_languages do
  Application.get_env(:cookie, :available_languages)
  |> Enum.map_join(&get_flag/1)
end

HOWEVER

That’s not going to get the plane home, it’s just going to get you to the next crash site.

Your config for available_languages stores the values as atoms. Calling byte_size(:en) fails, because atoms are not binaries:

iex(1)> byte_size(:en)
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not a bitstring

This typically happens when calling Kernel.byte_size/1 with an invalid argument or when performing binary construction or binary concatenation with <> and one of the arguments is not a binary
    :erlang.byte_size(:en)

The way to fix this depends on the overall context of how the value is used. Either the value passed to get_flag need to be converted (from atom → string), or the value kept on the user needs to be kept consistently as a string.

2 Likes

Thanks a lot, You are all amazing! I hope that I understood correctly, unfortunately I’m still not done. Now my code looks like this:

      def available_languages, do: Application.get_env(:cookienovo, :available_languages)
      |> Atom.to_string()
      |> Enum.map_join(&FlagsHelpers.get_flag()/1)

So first I have to write a function that splits the list into atoms and then converts the atoms to a string and then puts it in get_flag. Is it correct?

I don’r know if you are doing the right thing. But your error is raised, as you are passing a list of values to Atom.to_string. But it expects an Atom, as the module name implies. A quick fix would be to iterate of the list. Something like |> Enum.map(&Atom.to_string/1).

1 Like

Thank you a lot, so updates are.

available_languages I have define here in config.exs:

config :cookie,
  available_languages: ~w(gb sk)a

In cookie_web.ex I have:

      def available_languages, do: Application.get_env(:cookie, :available_languages)
      |> Enum.map(&Atom.to_string/1)
      |> Enum.map_join(&CookieWeb.FlagsHelpers.get_flag()/1)

I think I’m one step closer again :slight_smile:

My ongoing issues are currently:

And I think the problem will be somewhere here in html.heex.

         <div id="lang-menu-bar" class="hidden origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" role="menu" aria-orientation="vertical" aria-labelledby="user-menu-button" tabindex="-1">
            <%= for lang <- available_languages() do %>
              <%= link Gettext.gettext(CookieWeb.Gettext, to_string(lang)), to: Routes.lang_path(@conn, :set, lang), class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 #{if lang == @current_user.default_lang, do: 'font-bold'}", role: "menuitem", tabindex: -1 %>
            <% end %>
          </div>

Thank you so much everyone for your help, you have helped me understand things a little better.

An important skill for working with Elixir code is understanding what “shape” of data the different parts expect.

A construct like for is expecting a list (or more generally, something that the functions in Enum work on).

The error message is telling you that instead of a list, you’re providing a bitstring with two flags in it.

Where is that coming from? Reading into available_languages, that’s because Enum.map_join transforms the list and combines the strings with Enum.join.

This is not the behavior you want, since the for loop is expecting a list.

Changing the map_join to a plain map will get you closer, but the next line in your template won’t be happy either:

<%= link Gettext.gettext(CookieWeb.Gettext, to_string(lang)), to: Routes.lang_path(@conn, :set, lang), class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 #{if lang == @current_user.default_lang, do: 'font-bold'}", role: "menuitem", tabindex: -1 %>
  • Routes.lang_path is likely not expecting a flag emoji, but instead something like "en".

  • @current_user.default_lang is likely not set to a flag emoji, but instead something like :en.

  • Gettext.gettext likely doesn’t have a translation for a flag emoji, but was useful before this change to transform a string like "en" into a human-readable "English"

Overall, this set of facts leads me to one conclusion: most of this template doesn’t want available_languages to have flag emoji in it. An alternative approach would be to revert available_languages to its simple form:

def available_languages, do: Application.get_env(:cookie, :available_languages)

and then call get_flag in the template:

<%= for lang <- available_languages() do %>
  <%= link get_flag(to_string(lang)), to: Routes.lang_path(@conn, :set, to_string(lang)), class: "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 #{if lang == @current_user.default_lang, do: 'font-bold'}", role: "menuitem", tabindex: -1 %>
<% end %>

Some of these `to_string` calls could be tidied up; `lang_path` will convert non-strings to strings, and `get_flag` could be modified to do `to_string` internally, if country codes are typically represented as atoms.

Just wanted to point out the elephant in the room, which is that you can’t reliably deduce a country flag from a language, as some languages are spoken in many countries, and many countries use multiple languages. :slight_smile: