dialecto_in_context

In-context editing for Phoenix apps translated with gettext, for use with Dialecto.

While the editor is open, every translated string on a page of your running dev app can be clicked and edited in a Dialecto sidebar, with the gettext rules intact: contexts (pgettext), one field per plural form, and %{name} placeholders. Your edits become Dialecto's usual minimal-diff pull request against your translation files.

By default this is a dev-only dependency. The two opt-in lines in Setup compile to nothing where the dependency is absent, so production carries no trace of it. A site that wants editors to work on the live site can run it in production too; see "Editing the live site", which changes nothing for visitors.

What it does

The add-on does three things in your dev server:

What Dialecto reads, and what it never reads

Dialecto reads only your translation files, never your source code. The Dialecto GitHub App reads only the translation-file paths confirmed for your project. Each time it reads, it also matches the names in the repository's file listing, never their contents, against a published pattern list, and shows the project's managers any translation files it finds outside those paths; nothing under them is read until someone adds the path. This add-on does not widen that: it never reads, scans or uploads your source code or your catalogs.

The pattern list is published in the Dialecto repository at lib/dlocal/localize/catalog_discovery.ex (patterns/0, patterns/1 and excluded_folders/0). For gettext it looks for **/LC_MESSAGES/*.po and **/*.pot; for other formats it looks for **/config/locales/**/*.yml, **/_locales/*/messages.json and JSON files named after your source locale. Folders such as node_modules, vendor, build, test and anything starting with a dot are never proposed.

That is an improvement over reading the whole source tree, which GitHub's permission would allow. One caveat is part of the design: GitHub's Contents permission applies to the whole repository, so GitHub does not enforce this boundary. Dialecto's own code does. A team that wants the boundary enforced outside Dialecto's code can block the App's read access and let its CI push the translation files instead (CI-push mode); the generated workflow lists exactly which paths it sends, in your own repository.

The catalogs reach Dialecto from a push (the App reads the confirmed paths) or from your CI. The add-on itself never sends them.

What runs where

On your machine, in your dev server:

In your browser, from Dialecto:

The add-on makes no outbound request of its own. Your Elixir code never calls Dialecto's servers.

Setup

  1. Add the dependency for :dev only:

    # mix.exs
    {:dialecto_in_context, "~> 0.1", only: :dev}

    A local checkout also works: {:dialecto_in_context, path: "../dialecto_in_context", only: :dev}.

  2. Mark the gettext backend, right after use Gettext.Backend:

    # lib/my_app_web/gettext.ex
    defmodule MyAppWeb.Gettext do
      use Gettext.Backend, otp_app: :my_app
    
      if Code.ensure_loaded?(DialectoInContext.Gettext),
        do: @before_compile(DialectoInContext.Gettext)
    end
  3. Add the plug to the endpoint, before the router and before Plug.Parsers, so it reads its own small JSON bodies:

    # lib/my_app_web/endpoint.ex
    if Code.ensure_loaded?(DialectoInContext.Plug), do: plug(DialectoInContext.Plug)
  4. Recompile the backend (mix compile --force, or touch the file) and run mix phx.server. An "Edit text" button appears at the bottom right of every page.

The add-on finds the project on its own: it reads the app's git remote and Dialecto matches it to your project. Requirements: Elixir 1.18 or later, gettext ~> 1.0, expo ~> 1.0 and plug ~> 1.15.

Configuration

An environment variable wins over config :dialecto_in_context, ..., which wins over the default. The names match the Astro add-on's.

Option Env var Default What it is for
url DIALECTO_URL https://app.dialecto.eu The Dialecto that serves the overlay and sidebar. Set it when you run Dialecto yourself, for example http://localhost:4500.
project DIALECTO_PROJECT none The project's owner/name slug or its number. Use it when the git remote doesn't identify the project, or when several projects share one repository.
catalogs DIALECTO_CATALOGS priv/gettext The catalogs folder whose uncommitted files the sidebar warns about.
enabled DIALECTO_IN_CONTEXT on enabled: false or DIALECTO_IN_CONTEXT=off (also false, 0 or no) turns the add-on off.
paths DIALECTO_PATHS every page Comma-separated path prefixes whose pages get the editor, to keep it off admin screens, say.
exclude_paths DIALECTO_EXCLUDE_PATHS none Comma-separated path prefixes whose pages never get the editor, even under paths. Use it when you want every page but a few.
# config/dev.exs
config :dialecto_in_context, url: "http://localhost:4500", paths: ["/app"]

Editing the live site

The same add-on can run in production, inert for visitors and on only for an editor who arrives with a signed launch link from Dialecto ("Edit on live site"). An edit made this way is an ordinary staged edit in Dialecto; the site never writes anything itself.

When to use it

Live-site editing costs visitors nothing: no Dialecto script, no markers, the same bytes. It suits Phoenix sites too, not only static ones.

Setup

  1. Drop only: :dev from the dependency, so it is compiled in production. The guarded lines in "Setup" above keep working: they compile away only where the dependency is absent.

  2. Put the plug after Plug.Session, still before the router:

    plug Plug.Session, @session_options
    if Code.ensure_loaded?(DialectoInContext.Plug), do: plug(DialectoInContext.Plug)
  3. Add the hook to your LiveView sessions:

    live_session :default, on_mount: [DialectoInContext.LiveView] do
      live "/", HomeLive
    end
  4. Turn it on with the settings from the repo's Dialecto settings page. They are public and safe to commit:

    # config/runtime.exs (or config/prod.exs)
    config :dialecto_in_context,
      mode: :live,
      repo: "owner/name",
      origin: "https://www.example.com",
      public_key: "ed25519:…"

    How a release gets :live: mode is read at runtime, so it needs no build flag, but it is never on by itself. Unset, it is :dev in a dev or test build and :off in any other (decided when the package compiles), so a production release is inert until its config sets mode: :live. In :live the plug does nothing without a repo and a valid public_key.

    origin is the site's own origin. It is never read from Host or X-Forwarded-*, which a proxy can rewrite; if you leave it out, the plug uses the endpoint's configured URL. If the app sends a CSP header, the plug adds Dialecto (url) to frame-src on an editor's pages, and only on those.

What happens

A launch token that leaks is worth at most eight hours of seeing marked pages: it does not let anyone change text. Editing needs Dialecto's own sign-in, scoped to the repo and the site.

Refreshing the overlay copy

priv/static/overlay.js is a copy of Dialecto's built overlay. Maintainers refresh it with mix esbuild in_context in the Dialecto repository and then mix dialecto_in_context.refresh_overlay here. A test in the Dialecto repository fails when the copy differs from a fresh build.

Safety

These describe development mode. Live mode's rules are under "Editing the live site".

Run your own fork

You can run your own version of this package.

  1. Fork the repository.

  2. Depend on your fork, pinned to a commit:

    {:dialecto_in_context, git: "https://github.com/your-org/your-fork.git", ref: "<commit-sha>", only: :dev}

    or, from a checkout, {:dialecto_in_context, path: "../your-fork", only: :dev}.

What the fork covers is the Elixir package: the gettext wrapper, the plug, the git context and the settings. It has no hidden dependencies on Dialecto's servers; its Hex dependencies are gettext, expo and plug. What it does not include is the overlay and the sidebar. Those are served by a Dialecto instance, which is https://app.dialecto.eu unless you set url. A fork still needs a Dialecto to load them from, hosted by Dialecto or run by you with url pointing at it.

Develop

mix deps.get
mix test

See AGENTS.md for the layout, conventions and the rules to keep.

File Role
lib/dialecto_in_context.ex Entry point and version.
lib/dialecto_in_context/gettext.ex The @before_compile backend wrapper, plural selection and draft overrides.
lib/dialecto_in_context/plug.ex The /__dialecto/* routes, the origin rules and the loader injection.
lib/dialecto_in_context/config.ex Settings from the environment and app config.
lib/dialecto_in_context/{marker,args}.ex The invisible marker codec and the argument payload.
lib/dialecto_in_context/{store,overrides}.ex Marking state and saved drafts, in an ETS table.
lib/dialecto_in_context/git_context.ex The git remote, branch, commit and uncommitted catalogs.

License

No license is declared yet. A license will be added before the first release.

Security

Please report vulnerabilities privately and not in a public issue. Security contact: to be announced before the first release. Until then, report to the maintainers listed on the repository.

Contributing

Issues and pull requests are welcome. Keep changes small and tested, and read AGENTS.md first: it lists the behavior that cannot change without a major version.