tutorial

How to Monitor ex_cldr with Vigilmon

--- title: How to Monitor ex_cldr with Vigilmon published: true description: Monitor your Elixir i18n stack built with ex_cldr — detect locale data compilat...


title: How to Monitor ex_cldr with Vigilmon published: true description: Monitor your Elixir i18n stack built with ex_cldr — detect locale data compilation failures, number and date formatting errors, and missing locale coverage with Vigilmon heartbeats. tags: elixir, i18n, cldr, monitoring

ex_cldr is the comprehensive internationalization library for Elixir, implementing the Unicode CLDR standard for formatting numbers, currencies, dates, times, and plural forms across hundreds of locales. It compiles locale data at build time into optimized Elixir modules — so there is no runtime locale file loading, no JSON parsing on the hot path, and no locale-not-found errors in production. When it works, it is invisible. When it breaks, every user-facing string in every locale breaks with it.

Monitoring ex_cldr means monitoring the build-time compilation step, the runtime formatting calls, and the coverage of locales your app actually serves. Vigilmon heartbeats catch compilation failures and coverage regressions before they reach production.


Why Monitor ex_cldr?

ex_cldr compiles locale data into your OTP release. That compilation can fail silently in CI when locale configuration drifts from the actual locales used in templates:

  • Missing locale in backend config — a template calls Cldr.Number.to_string!/2 with a locale not listed in your backend locales: config; compilation succeeds but the call raises at runtime
  • Locale data version mismatch — upgrading ex_cldr pulls new CLDR data; tests using hardcoded formatted strings break without obvious connection to the upgrade
  • Fallback locale not configured — an unsupported locale reaches your formatter; without a fallback, users see an exception instead of formatted output
  • Currency format drift — a CLDR data update changes the format pattern for a currency your app displays; regression tests catch this but only if they run
  • Plural rule edge cases — ex_cldr plural rules handle one, two, few, many, other — plural forms vary by locale in non-obvious ways, and untested locales expose edge-case bugs

Key Metrics to Track

| Metric | What it tells you | |--------|-------------------| | Backend compilation success | Whether the CLDR backend compiles without errors in CI | | Locale coverage (%) | Percentage of supported locales with test coverage | | Runtime formatting error rate | How often Cldr.Number.to_string/2 or date formatters raise in production | | Fallback locale hit rate | How often requests fall back to the default locale | | CI heartbeat | Whether locale smoke tests ran recently |


Step 1: Configure Your ex_cldr Backend

# mix.exs
defp deps do
  [
    {:ex_cldr, "~> 2.38"},
    {:ex_cldr_numbers, "~> 2.33"},
    {:ex_cldr_dates_times, "~> 2.20"},
    {:ex_cldr_currencies, "~> 2.16"},
    {:jason, "~> 1.4"}
  ]
end

Define your backend module with the locales your app supports:

# lib/my_app/cldr.ex
defmodule MyApp.Cldr do
  use Cldr,
    locales: ["en", "de", "fr", "es", "pt", "ja", "zh", "ar"],
    default_locale: "en",
    providers: [Cldr.Number, Cldr.DateTime, Cldr.Currency],
    generate_docs: false
end

Test the backend compiles and formats correctly:

mix compile
iex -S mix
iex> MyApp.Cldr.Number.to_string!(1_234_567.89, locale: "de")
# "1.234.567,89"
iex> MyApp.Cldr.Number.to_string!(1_234_567.89, locale: "en", currency: :USD)
# "$1,234,567.89"

Step 2: Write Locale Coverage Tests

Add a test that verifies formatting works across every supported locale:

# test/my_app/cldr_test.exs
defmodule MyApp.CldrTest do
  use ExUnit.Case, async: true

  @number_fixture 1_234_567.89
  @date_fixture ~D[2026-07-03]

  describe "number formatting" do
    for locale <- ["en", "de", "fr", "es", "pt", "ja", "zh", "ar"] do
      @locale locale

      test "formats numbers in #{locale}" do
        assert {:ok, result} = MyApp.Cldr.Number.to_string(@number_fixture, locale: @locale)
        assert is_binary(result)
        refute result == ""
      end

      test "formats currency in #{locale}" do
        assert {:ok, result} =
                 MyApp.Cldr.Number.to_string(@number_fixture,
                   locale: @locale,
                   currency: :USD,
                   format: :currency
                 )

        assert is_binary(result)
      end
    end
  end

  describe "date formatting" do
    for locale <- ["en", "de", "fr", "ja"] do
      @locale locale

      test "formats dates in #{locale}" do
        assert {:ok, result} =
                 MyApp.Cldr.DateTime.to_string(@date_fixture, locale: @locale, format: :long)

        assert is_binary(result)
        refute result == ""
      end
    end
  end

  describe "plural rules" do
    test "en uses one/other" do
      assert MyApp.Cldr.Number.PluralRule.plural_type(1, locale: "en") == :one
      assert MyApp.Cldr.Number.PluralRule.plural_type(2, locale: "en") == :other
    end

    test "ar uses six plural forms" do
      assert MyApp.Cldr.Number.PluralRule.plural_type(0, locale: "ar") == :zero
      assert MyApp.Cldr.Number.PluralRule.plural_type(1, locale: "ar") == :one
      assert MyApp.Cldr.Number.PluralRule.plural_type(2, locale: "ar") == :two
      assert MyApp.Cldr.Number.PluralRule.plural_type(11, locale: "ar") == :many
    end
  end
end

Step 3: Add a Health Endpoint with Locale Checks

Include a formatting spot-check in your health endpoint:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  def init(opts), do: opts

  def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
    checks = %{
      database: check_database(),
      cldr: check_cldr()
    }

    status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503

    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(%{status: status_label(status), checks: checks}))
    |> halt()
  end

  def call(conn, _opts), do: conn

  defp check_cldr do
    with {:ok, _} <- MyApp.Cldr.Number.to_string(1_234.56, locale: "en"),
         {:ok, _} <- MyApp.Cldr.Number.to_string(1_234.56, locale: "de") do
      :ok
    else
      {:error, _} -> :error
    end
  end

  defp check_database do
    case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
      {:ok, _} -> :ok
      {:error, _} -> :error
    end
  end

  defp status_label(200), do: "ok"
  defp status_label(_), do: "degraded"
end

Step 4: Run Locale Tests in CI with a Heartbeat

# .github/workflows/i18n.yml
name: i18n Locale Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  locale-tests:
    name: ex_cldr Locale Coverage
    runs-on: ubuntu-latest
    env:
      VIGILMON_CLDR_HEARTBEAT_URL: ${{ secrets.VIGILMON_CLDR_HEARTBEAT_URL }}
    steps:
      - uses: actions/checkout@v4

      - uses: erlef/setup-beam@v1
        with:
          elixir-version: '1.16'
          otp-version: '26'

      - name: Cache deps
        uses: actions/cache@v3
        with:
          path: deps
          key: ${{ runner.os }}-deps-${{ hashFiles('mix.lock') }}

      - run: mix deps.get

      - name: Compile (catches CLDR backend errors)
        run: mix compile --warnings-as-errors

      - name: Run locale coverage tests
        run: mix test test/my_app/cldr_test.exs --color

      - name: Ping Vigilmon heartbeat
        if: success()
        run: curl -fsS "$VIGILMON_CLDR_HEARTBEAT_URL"

Step 5: Instrument Runtime Formatting Errors

Track when formatting calls fail in production:

# lib/my_app/safe_cldr.ex
defmodule MyApp.SafeCldr do
  require Logger

  @doc "Format a number for display, falling back to raw string on error."
  def format_number(value, opts \\ []) do
    locale = Keyword.get(opts, :locale, "en")

    case MyApp.Cldr.Number.to_string(value, opts) do
      {:ok, formatted} ->
        formatted

      {:error, {_exception, message}} ->
        Logger.warning("CLDR number format failed",
          locale: locale,
          value: value,
          error: message
        )

        :telemetry.execute(
          [:my_app, :cldr, :format_error],
          %{count: 1},
          %{locale: locale, formatter: :number}
        )

        to_string(value)
    end
  end

  @doc "Format a date for display, falling back to ISO 8601 on error."
  def format_date(date, opts \\ []) do
    locale = Keyword.get(opts, :locale, "en")

    case MyApp.Cldr.DateTime.to_string(date, opts) do
      {:ok, formatted} ->
        formatted

      {:error, _} ->
        Logger.warning("CLDR date format failed", locale: locale, date: date)
        Date.to_iso8601(date)
    end
  end
end

Step 6: Create a Heartbeat Monitor in Vigilmon

  1. Sign in at vigilmon.online
  2. Click New Monitor → Heartbeat
  3. Name it ex_cldr Locale Tests — main branch
  4. Set the expected interval to 25 hours
  5. Copy the URL and store it as VIGILMON_CLDR_HEARTBEAT_URL in CI secrets

If a CLDR backend upgrade breaks locale compilation or formatting tests fail, no heartbeat is sent and Vigilmon alerts you before a broken release ships.


Step 7: Alerting

In Vigilmon, configure Notifications → New Channel → Slack:

Heartbeat missed alert:

🔴 MISSED: ex_cldr Locale Tests — main branch
Last passing i18n tests: 30 hours ago
Action: Check CI — possible CLDR backend compilation failure or locale coverage regression

HTTP health alert (if CLDR smoke check fails):

🔴 DOWN: yourdomain.com/health
Status: 503 — CLDR formatting check failed

What You Built

| What | How | |------|-----| | CLDR backend compilation check | CI step with --warnings-as-errors | | Locale coverage tests | ExUnit tests across all supported locales | | Plural rule tests | Tests for edge-case plural forms per locale | | CLDR health check | Formatting spot-test in /health plug | | CI heartbeat | Vigilmon heartbeat — alerts when locale tests miss | | Runtime error tracking | Telemetry + Logger on format failures | | Slack alerting | Vigilmon Slack notification channel |

ex_cldr compile-time approach eliminates runtime locale loading. Vigilmon ensures that compilation step and its tests never silently fail.


Next Steps

  • Add locale acceptance tests that verify output strings directly (not just non-empty) to catch format pattern drift across CLDR upgrades
  • Track [:my_app, :cldr, :format_error] telemetry in your APM tool to detect locale-specific formatting failures in production
  • Add a separate heartbeat for currency-specific tests if your app handles payments in multiple currencies
  • Use Vigilmon response time monitoring on your health endpoint to catch slow CLDR initialization on cold starts

Get started free at vigilmon.online.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →