Locallingo

AI-assisted i18n translation, drift detection, and quality linting on top of i18n-tasks — packaged as a gem so the same toolchain works across every app instead of being copy-pasted (and forking) per repo.

Locallingo:

  • Translates missing/changed keys through RubyLLM (OpenAI, Anthropic, Google, … — you pick the provider/model).
  • Detects drift with per-key source hashes, so only genuinely missing/outdated keys are re-translated.
  • Validates translation completeness for CI (missing, outdated, and optional duplicate-value / manual-edit checks).
  • Lints quality — static rules, terminology lists, optional British-spelling drift, plus an optional AI review.
  • Ships RuboCop cops that enforce fully-qualified i18n keys and locale-aware date formatting.

Everything app-specific lives in .locallingo.yml — target locales, provider and model, prompt context/glossary, per-language style guides, which validators and quality rules run, and post-translate hooks — with a default block plus optional per-package overrides.

Installation

# Gemfile
group :development do
  gem "locallingo"
end

RubyLLM reads provider credentials from ENV (e.g. OPENAI_API_KEY, ANTHROPIC_API_KEY). Locallingo never stores keys.

Configuration

Create .locallingo.yml at your app root:

defaults:
  source_locale: en
  target_locales: [de, sv]

  provider: openai            # any RubyLLM provider
  translate:
    model: gpt-5-mini
  quality:
    model: gpt-4o-mini
    terminology: business     # "business" | "banking" | path/to/terms.yml
    british_spellings: false

  context: "Cosmos, a business application"
  glossary:
    entity: "business/company account holder"
    member: "user belonging to an entity"
  language_guides:
    de: config/locales/.guides/de.md   # inline text or `file:` path

  validators:
    missing: true
    outdated: true
    duplicate_values: true    # flags non-AR keys duplicating an AR attribute value
    manual_edits: false       # protects hand-edited target values

  strict:
    strict:     [missing, outdated]
    strict_all: [missing, outdated, duplicate_value]

  after_translate:
    - "bundle exec i18n-tasks normalize -p"

# Optional per-location overrides (each inherits `defaults`):
packages: []
#  - path: engines/billing
#    context: "the billing engine"
#    target_locales: [de]

With no packages:, the whole app uses defaults — the common case.

CLI

lingo status                       # translation status per locale
lingo translate --locale de        # translate missing/changed keys
lingo translate --force            # re-translate everything
lingo validate --strict            # CI gate (exit 1 on strict-tier issues)
lingo validate --strict-all        # stricter CI gate
lingo quality --ai                 # quality linting (+ optional AI pass)
lingo fix-quality --locale en      # auto-fix fixable issues
lingo accept-edits --locale de     # mark current translations as intentional
lingo sync                         # rebuild drift state from current files
lingo hash                         # source-translation fingerprint

Add --package engines/billing to any command to scope it to a package.

The legacy flag forms (lingo --translate, lingo --validate, …) still work and print a one-line deprecation notice, so you can migrate CI and docs incrementally.

RuboCop cops

# .rubocop.yml
require:
  - locallingo/rubocop
inherit_gem:
  locallingo: config/default.yml
  • Locallingo/RelativeI18nKey — flags relative t(".key") lookups and autocorrects them to fully-qualified keys derived from the file path and enclosing method. Configure ScopedDirectories for your layout.
  • Locallingo/StrftimeInView — flags .strftime(...) in views; use I18n.l(value, format: :name) instead. value:-pair inputs (HTML datetime) are exempt.

Locallingo recommends disabling Rails' Rails/I18nLazyLookup and Rails/I18nLocaleTexts, which fight the fully-qualified-key convention.

Programmatic use

config = Locallingo.configuration(root_path: Dir.pwd)
Locallingo::Manager.new(config:).validate           # => [violations]
Locallingo::QualityChecker.new(config:).check(locale: "en")

License

MIT © Mikael Henriksson