countries / countries/countries

Country.translations re-reads and re-parses the locale JSON on every call

Open
#969 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
2.4k
Forks
682
Avg merge
5d 4h
Merged PRs (30d)
2

Description

(Sorry for the AI generated text but I ran into this during a performance analysis in my app and Claude discovered this issue)

Summary

ISO3166::Country.translations(locale) does a File.read + JSON.parse of
locales/<locale>.json on every invocation, with no memoization.
Country.all_translated delegates to it and is affected too.

By contrast, the instance method Country#translation(locale) reads from the
in-memory ISO3166::Data cache and does no I/O — so the class-level API is thea
inconsistent one.

https://github.com/countries/countries/blob/master/lib/countries/country/class_methods.rb#L60-L68

def translations(locale = :en)
  locale = locale.to_sym if locale.is_a?(String)
  locale = locale.downcase if locale.match?(/[A-Z]/)

  file_path = ISO3166::Data.datafile_path(%W[locales #{locale}.json])
  translations = JSON.parse(File.read(file_path))

  translations.merge(custom_countries_translations(locale))
end

Reproduction

require 'countries'

$reads = 0
File.singleton_class.prepend(Module.new do
  def read(*a, **k); $reads += 1 if a.first.to_s.end_with?('.json'); super; end
end)

ISO3166::Country.translations(:en) # warm
$reads = 0
100.times { ISO3166::Country.translations(:en) }
puts "file reads, 100x translations(:en):     #{$reads}"

$reads = 0
100.times { ISO3166::Country.all.map { |c| c.translation(:en) } }
puts "file reads, 100x Country#translation:   #{$reads}"

Results

file reads, 100x translations(:en):     100
file reads, 100x Country#translation:   0

translations(:en)      0.0547 ms/call
all_translated(:en)    0.0484 ms/call

en.json: 4636 bytes, 249 entries

Why it matters

Building a country select list is a common use of this API. In our app the call
sits in an object that is constructed per request, so every page render does a
filesystem read and a 4.6 KB JSON parse to produce a list that cannot change
within the process. The per-call cost is small, but it is unbounded in call
count and it is I/O rather than CPU.

Callers can memoize it themselves (we now do), so this isn't blocking us. The
surprising part is that the class-level API does I/O while the instance-level
equivalent is cached.

Notes on a possible fix

Memoizing isn't a one-liner, so rather than assume, some tradeoffs worth
deciding first:

  1. Mutation. translations currently returns a fresh, unfrozen Hash on
    every call. Memoizing and returning the same object would let one caller's
    mutation leak into every other caller. Options are to freeze the cached hash
    (a breaking change for anyone mutating the result) or to dup on return
    (still avoids the read + parse, and is cheaper than today).
  2. Invalidation. The result merges custom_countries_translations(locale),
    so the cache would need clearing on ISO3166::Data.register / unregister
    / reset.
  3. Thread safety. ISO3166::Data.synchronized already exists to build on.

Happy to open a PR if you have a preference on the mutation question
(freeze vs. dup).

Environment

  • countries 8.1.0 (latest release; master is identical as of 2026-09-09)
  • Ruby 4.0.6
  • Linux

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in lib/countries/country/class_methods.rb at translations, then inspect ISO3166::Data.synchronized and the register, unregister, and reset paths mentioned in the issue. Reproduce the repeated reads with the provided script. Done means repeated translations calls avoid unnecessary file reads while preserving the stated mutation, invalidation, and thread-safety behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
backend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.