Emacs avec correction orthographique, grammaire et traduction française pour NixOS

Table des matières

Dans cet article, je vous propose une configuration NixOS/Home Manager pour installer Emacs et avoir de la correction d'orthographe et de grammaire, ainsi qu'une fonction de traduction.

Je ne suis pas un Gourou d'Emacs, donc la configuration peut probablement être améliorée.

Erreurs de grammaire et orthographe soulignées dans Emacs.
Le résultat final.

Pré-requis

Pour mettre en œuvre la configuration de cet article, vous devez utiliser Home-manager. Cet article a été écrit sur un ordinateur tournant sous NixOS, non testé sur d'autres distributions Linux ou MacOS.


Utilisation

Choix de la langue

Lorsque vous visitez un fichier, la langue est détectée automatiquement. Dans le cas d'un nouveau fichier vide, vous pouvez choisir la langue vous-même avec la commande M-x ispell-change-dictionary.

Si l'auto détection par la suite ne convient pas, la langue peut être forcée avec des variables de fichier. Par exemple, dans un document HTML, les lignes suivantes forceront la langue à "american":

A la première ligne du fichier:

<!-- -*- ispell-local-dictionary: "american"; -*- -->
<html>
 <body>The big aardvark jumped over the lazy fox while it was sleeping.</body>
</html>

Ou bien en bas du fichier:

<html>
 <body>The big aardvark jumped over the lazy fox while it was sleeping.</body>
</html>
<!-- 
    Local Variables:
    ispell-local-dictionary: "american"
	End:
-->

Correction des erreurs

Les erreurs d'orthographe et de grammaire (pour les documents français) sont soulignées dans le document. Pour les corriger, vous pouvez faire un clic droit dessus. Alternativement, déplacer le curseur texte sur un mot et utiliser le raccourci clavier C-;.

Traduction

Pour traduire un texte, le souligner avec la souris ou C-espace. Puis utiliser le raccourci clavier C-c t. Le sens de traduction peut être inversé en activant le tampon de traduction et en appuyant sur la touche t.


Présentation des paquets Emacs utilisés

Flycheck

Flycheck lance des outils de vérification sur un texte et suivant les erreurs qu'il rapporte, souligne les mots erronés. Nous allons utiliser Flycheck-aspell qui détectera les fautes d'orthographe en utilisant Aspell et aussi Flycheck-grammalecte un paquet qui lance le correcteur de grammaire française Grammalecte pour trouver les erreurs de grammaire.

Flyspell

Flyspell est un outil de correction de l'orthographe. Là ou Flycheck ne fait qu'afficher les mots erronés, Flyspell lui propose des fonctions pour les corriger, comme des menus graphiques ou textuels.

Guess-language

Guess-language est un détecteur de langue qui normalement change la langue de Ispell à la volée d'un paragraphe à l'autre. Nous désactivons cela pour uniquement utiliser sa fonction de détection lors de la visite d'un fichier.

Go Translate

Go Translate permet de traduire du texte d'une langue à une autre en appelant diverses API pour traduire, comme Google Translate ou Bing. Nous allons utiliser Libre Translate, un système de traduction libre qui peut être installé sur le poste pour éviter d'avoir à payer un abonnement et envoyer son texte à des services tiers ; Libre Translate utilise Argos Translate en interne un moteur de traduction utilisant des réseaux de neurones.

Intégration des paquets

Nous allons mettre en place une interface unique pour corriger les erreurs Aspell ou Grammalecte. Avec clic droit sur les mots soulignés ou bien appui sur les touches contrôle et point-virgule (C-;).

Pour cela les raccourcis claviers et souris installés par les paquets sont supprimés et remplacés par deux fonctions qui se chargent de voir quel correcteur appeler suivant la nature de l'erreur.

    flowchart LR
	  aspell[Aspell]
	  grammalecte[Grammalecte]
	  flycheck[Flycheck]
	  flycheckaspell[Flycheck-aspell]
	  flyspell[Flyspell]
 	  flycheckgrammalecte[Flycheck-grammalecte]
  	  tampon[Tampon Emacs]
	  fonctions[Clic droit ou raccourci clavier]
	  aspell -->|Erreurs| flycheckaspell
	  flycheckaspell -->|Erreurs formattées|flycheck	  
	  grammalecte -->|Erreurs| flycheckgrammalecte
	  flycheckgrammalecte -->|Erreurs formattées|flycheck
	  flycheck -->|Souligne les erreurs| tampon  
	  flycheckgrammalecte -->|Affiche un menu de correction| fonctions
   	  flyspell -->|Affiche un menu de correction| fonctions
	  fonctions -->|Fournissent une interface de correction|tampon

Le plus gros de la configuration est fait dans le fichier init.el ci-dessous.

init.el
;;; -*- lexical-binding: t -*-
;;; init.el --- Initialization file for Emacs
;;; Commentary: Emacs Startup File --- initialization for Emacs

(require 'package)
(require 'use-package)

;;; On définit ici les langues avec lesquelles on souhaite travailler.
;;; On associe le code langue (détecté par guess-language) au dictionnaire
;;; à utiliser et au nom de la langue.
;;; Mettez en premier la langue que vous souhaitez être par défaut si
;;; l'auto détection échoue.

(setq my/langues
  '(
    (fr . ("french" "Français"))
    (en . ("english" "English"))
  )
)

;;; Langues pour le traducteur.
(setq my/langues-traduction '(fr en))

 ;;; Fonction qui vérifie qu'une erreur est de type Aspell.
(defun my/is-aspell-error (err)
  "Retourne vrai si l'erreur est de type Aspell"

  (string-match-p "aspell" (symbol-name (flycheck-error-checker err))))

(defun my/is-aspell-error-at-pos (pos)
  "Retourne vrai si l'erreur a la position est de type Aspell"

  (let* 
      (
       ;; Lit les erreurs Flycheck au point.
       (errors (flycheck-overlay-errors-at pos))
       ;; Regarder si c'est une erreur Aspell d'après le nom du vérificateur
       ;; ayant rapporté l'erreur.       
       (aspell-error (seq-find #'my/is-aspell-error errors)))
    aspell-error))

 ;;; Fonction qui regarde l'erreur située sous le clic souris.
 ;;; Si c'est une erreur Grammalecte, elle appelle la fonction de 
 ;;; correction Flycheck-grammalecte qui affiche une popup.
 ;;; Si c'est une erreur Aspell, idem, mais en appelant la fonction
 ;;; de correction Flyspell en popup.
(defun my/correct-at-click (event)
  "Corrige l'erreur Flycheck au clic"

  ;; Fonction qui peut être affectée aux clics et qui prend l'événement
  ;; souris en paramètre.
  (interactive "e")
  (save-excursion ; Restaurer la position du point (Le curseur texte.) à la fin.
    (mouse-set-point event) ; On déplace le point sous le clic souris.
    (if (my/is-aspell-error-at-pos (point))
      ;; Si c'est une erreur Aspell, afficher le popup Flyspell.
      (call-interactively 'flyspell-correct-word)

      ;; Si c'est une erreur Grammalecte, appeller sa fonction de correction
      ;; avec popup.
      (call-interactively 'flycheck-grammalecte-correct-error-at-click))))

 ;;; Cette fonction fonctionne comme la précédente, mais directement utilisable
 ;;; avec un raccourci clavier.
(defun my/correct-at-point (pos)
  "Corrige l'erreur Flycheck au point"

  (interactive "d")
  (if (my/is-aspell-error-at-pos pos)
      ;; Pour Aspell, appeler la fonction flyspell-correct configurée,
      ;; typiquement pour choisir le mot correct dans une liste textuelle.
      (call-interactively 'flyspell-correct-wrapper)

    ;; Sinon, utiliser la correction Grammalecte. Elle effectue la correction
    ;; proposée directement.
    (call-interactively 'flycheck-grammalecte-correct-error-at-point)))

 ;;; Cette fonction détecte la langue du tampon
(defun my/set-buffer-dictionary ()
  "Detecte la langue du tampin et renseigne le dictionnaire ispell local."

  (interactive)
  (unless ispell-local-dictionary
    (let ((lang (guess-language-buffer)))
      (when lang
        (ispell-change-dictionary 
         (cadr (assoc lang guess-language-langcodes)))
))))

 ;;; ===========================================================================
 ;;; Configuration Flycheck
 ;;;
 ;;; Flycheck est un paquet qui souligne les mots rapportés en erreur par ses
 ;;; "checkers".
 ;;; ===========================================================================

(use-package flycheck
  :ensure t
  :init 
  (global-flycheck-mode)

  :config
  ;; Si on n'est pas en mode graphique, changer la couleur des mots plutôt
  ;; que les souligner (ce qui ne fonctionne pas toujours suivant les polices
  ;; de caractères).
  (unless (display-graphic-p)
    (set-face-attribute 'flycheck-error nil
                        :underline nil
                        :background "red"
                        :foreground "white")
    (set-face-attribute 'flycheck-warning nil
                        :underline nil
                        :background "yellow"
                        :foreground "black"))

  ;; CTRL C -> correction Aspell ou Grammalecte en utilisant la fonction
  ;; définie plus haut.
  :bind (:map flycheck-mode-map ("C-;" . my/correct-at-point))
  )

 ;;; ===========================================================================
 ;;; Configuration Flyspell (correction orthographique)
 ;;; Ce paquet propose des mots possibles quand l'utilisateur choisit d'en
 ;;; corriger un.
 ;;; ===========================================================================

(use-package flyspell
  :ensure t
  :hook ((text-mode . flyspell-mode)
         (prog-mode . flyspell-prog-mode))

  :custom
  (ispell-program-name "aspell")
  (ispell-dictionary (cadar my/langues))

  ;; Neutraliser le raccourci clavier "C-;" installé par Flyspell.
  :bind (:map flyspell-mode-map ("C-;" . nil))
  )

 ;;; Le paquet flyspell-correct est une couche d'abstraction permettant
 ;;; d'utiliser des correcteurs différents. Par exemple un correcteur via popup,
 ;;; ou bien des menus textuels Helm ou Ivy.
(use-package flyspell-correct
  :ensure t
  )

 ;;; ===========================================================================
 ;;; Configuration Flycheck-aspell
 ;;; ===========================================================================

 ;;; Vérifier automatiquement le tampon quand on vient de sauver un mot dans
 ;;; le dictionnaire. Cette fonction est lancée après ispell-pdict-save 
 ;;; (voir ci-dessous).
(defun my/flycheck-recheck (&rest IGNORE)
  (when (bound-and-true-p flycheck-mode)
    (flycheck-buffer)))

 ;;; Flycheck-aspell lance Aspell sur le tampon et souligne les mots qui
 ;;; sont mal orthographiés. 
(use-package flycheck-aspell
  :ensure t

  :config
  ;; Définir un vérificateur pour les tampons sans mode (mode "fundamental") et
  ;; les fichiers texte.
  (flycheck-aspell-define-checker "text"
    "Text" ("--mode" "url")
    (fundamental-mode text-mode))

  ;; Ajouter tous les vérificateurs aspell.
  (dolist (checker '(c-aspell-dynamic
                     text-aspell-dynamic
                     html-aspell-dynamic
                     mail-aspell-dynamic
                     markdown-aspell-dynamic
                     nroff-aspell-dynamic
                     tex-aspell-dynamic
                     texinfo-aspell-dynamic
                     xml-aspell-dynamic))
    (add-to-list 'flycheck-checkers checker))

  ;; Vérifier le tampon après l'ajout d'un mot dans le dictionnaire.
  (advice-add #'ispell-pdict-save :after #'my/flycheck-recheck)
  ;; Vérifier le tampon après changement du dictionnaire.
  (advice-add #'ispell-change-dictionary :after #'my/flycheck-recheck)

  ;; Lancer une détection de la langue quand on ouvre un fichier
  (add-hook 'find-file-hook #'my/set-buffer-dictionary)
  )

 ;;; ===========================================================================
 ;;; Configuration Flycheck-grammalecte (correction grammaticale)
 ;;; ===========================================================================

 ;;; Correcteur de grammaire pour flycheck. Il souligne les endroits où se
 ;;; trouvent des erreurs de grammaire.
(use-package flycheck-grammalecte
  :ensure t
  :after flycheck-aspell

  :config 
  ;; Ces valeurs sont renseignées par le Flake Nix avec le chemin
  ;; du répertoire correct. Pour GRAMMALECTE_SITE, on doit faire une
  ;; recherche parmi les sous-répertoires pour trouver celui qui contient
  ;; les fichiers Lisp.
  (let* ((base-dir "@GRAMMALECTE_SITE@")
         (found-file (car (directory-files-recursively 
                           base-dir 
                           "flycheck-grammalecte\\.el")))
         (setq grammalecte--site-directory (file-name-directory found-file))))
  (setq grammalecte-python-package-directory "@GRAMMALECTE_PYTHON@")

  ;; Ne pas vérifier les mises à jour Grammalecte disponibles.
  (setq grammalecte-check-upstream-version-delay 0)

  ;; Activez ou non ces options suivant vos besoins.
  (setq flycheck-grammalecte-report-apos nil ; Apostrophes typographiques.
        flycheck-grammalecte-report-esp nil  ; Espaces et tabulations.
        flycheck-grammalecte-report-nbsp nil ; Espaces non sécables.
        flycheck-grammalecte-report-typo nil ; Symboles typographiques.
        )

  ;; Mise en place de Flycheck-grammalecte.
  (flycheck-grammalecte-setup)

  ;; Grammalecte installe une correction sur clic droit, on l'enlève, car
  ;; nous voulons notre propre fonction "my/correct-at-click" qui discerne
  ;; si l'erreur sur laquelle on clique vient de Aspell ou de Grammalecte.
  (keymap-unset flycheck-mode-map "<mouse-3>")
  (keymap-set flycheck-mode-map "<mouse-3>" #'my/correct-at-click)

  ;; Flycheck ne peut lancer qu'un "checker" à la fois. Ici, on dit
  ;; de lancer Grammalecte après Aspell. Et ce, pour chaque checker aspell.
  (dolist (checker '(c-aspell-dynamic
                     text-aspell-dynamic
                     html-aspell-dynamic
                     mail-aspell-dynamic
                     markdown-aspell-dynamic
                     nroff-aspell-dynamic
                     tex-aspell-dynamic
                     texinfo-aspell-dynamic
                     xml-aspell-dynamic))
    (flycheck-add-next-checker checker 'grammalecte))

  ;; Placer le vérificateur Grammalecte en fin de liste de Flycheck
  ;; pour qu'il ne prenne pas la place de Aspell, ce qui ferait que
  ;; Aspell ne serait jamais utilisé.
  (setq flycheck-checkers (delete 'grammalecte flycheck-checkers)
        flycheck-checkers (append flycheck-checkers '(grammalecte)))
  )

 ;;; ===========================================================================
 ;;; Configuration de Guess-language
 ;;; ===========================================================================

(use-package guess-language
  :ensure t
  :custom
  ;; Définir les langues à détecter à partir de notre liste.
  (guess-language-languages (mapcar #'car my/langues))
  (guess-language-langcodes
    (mapcar
      ;; Transforme notre liste de langue au format guess-language.
      (lambda (entry)
        (let* ((key (car entry))
               (values (cdr entry))
               (dict-name (car values))
               (display-name (cadr values))
               (key-string (symbol-name key)))
          (cons key (list dict-name nil key-string display-name))))
      my/langues))
  :config
  ;;Désactiver la détection automatique paragraphe par paragraphe.
  (remove-hook 'flyspell-incorrect-hook #'guess-language-function t)
  (remove-hook 'post-command-hook #'guess-language--post-command-h t)
  (advice-remove 'flyspell-buffer #'guess-language-flyspell-buffer-wrapper)
)

 ;;; ===========================================================================
 ;;; Configuration Go-translate
 ;;; ===========================================================================

 ;;; Go translate permet de traduire du texte. Il peut utiliser des services en 
 ;;; ligne, mais ici on appelle "Libre Translate" installé en service local.
(use-package gt
  :ensure t

  ;; Pour traduire une sélection: "CTRL c" puis "t".
  :bind ("C-c t" . gt-translate) 

  :custom
  ;; On définit le couple de langue par défaut.
  (gt-langs my/langues-traduction)
  ;; Utiliser Libre Translate installé en local.
  (gt-libre-host "http://localhost:5000/")

  :config
  ;; GT permet de créer des traducteurs personnalisés. Ici on défini un
  ;; traducteur qui opère sur une sélection texte (Faite avec "CTRL espace"
  ;; ou bien avec la souris).
  ;; Le résultat s'affiche dans un autre tampon ; il suffit ensuite de copier-coller.
  (setq gt-default-translator
        (gt-translator
         :taker   (gt-taker :text 'selection :pick nil :prompt nil)
         :engines (list (gt-libre-engine :key ""))
         :render  (gt-buffer-render)))
  )

(use-package flyspell-correct-ivy)

;; Local Variables:
;; fill-column: 80
;; display-fill-column-indicator-mode: t
;; ispell-dictionary: "francais"
;; End:

Voir les commentaires dans le fichier init.el pour plus d'informations.


Tester et voir si cela vous convient

Téléchargez l'archive contenant les fichiers si vous avez envie de tester. Vous pourrez alors lancer une machine virtuelle. Voir le fichier flake.nix ci-dessous.

flake.nix
{
  description = "Test Emacs avec support du Français";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
    home-manager = {
      url = "github:nix-community/home-manager/release-25.11";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, home-manager }: 
  let
    lib = nixpkgs.lib;
  in {
    nixosConfigurations.vm = lib.nixosSystem {
      system = "x86_64-linux";
      modules = [
        # Configuration NixOS minimale.
        ({ pkgs, ... }: {
          boot.loader.grub.device = "nodev";

          nix.settings = {
            experimental-features = "nix-command flakes";
	  };

          i18n.defaultLocale = "fr_FR.UTF-8";
          console.keyMap = "fr";

          services.xserver = {
	    enable = true;
            xkb.layout = "fr";

            windowManager.i3 = {
              enable = true;
            };

            displayManager = {
              lightdm.enable = true;
	      autoLogin = {
                enable = true;
                user = "test";
              };
            };
          };
	  services.displayManager = {
	    defaultSession = "none+i3";
	    autoLogin = {
              enable = true;
              user = "test";
            };
	  };

          fileSystems."/" = {
            device = "/dev/vda";
            fsType = "ext4";
          };

          # Utilisateur "test" avec mot de passe "test".
          users.users.test = {
            isNormalUser = true;
            password = "test";
            extraGroups = [ "wheel" ];
          };

          # Activer sudo sans mot de passe pour wheel.
          security.sudo.wheelNeedsPassword = false;

          # Configuration réseau de base.
          networking.hostName = "emacs-vm";
          networking.useDHCP = true;

          # Nécessaire pour la VM.
          virtualisation.vmVariant.virtualisation = {
            diskSize = 2048;
            memorySize = 4096;
            cores = 2;
            resolution = { x = 1200; y = 800; };
          };

          system.stateVersion = "25.11";
        })

        # Configuration Home Manager.
        home-manager.nixosModules.home-manager
        {
          home-manager.useGlobalPkgs = true;
          home-manager.useUserPackages = true;
          home-manager.users.test = { pkgs, ... }: 
          let
             # Aspell, le correcteur d'orthographe. Avec des dictionnaires anglais et 
             # français pour l'exemple.
             aspell = (pkgs.aspellWithDicts (dicts: with dicts; [ en en-computers fr ]));
             # Grammalecte, le correcteur de grammaire.
             grammalecte = pkgs.python313Packages.grammalecte;
          in {
            # Le service Libre Translate utilisateur.
	    imports = [ ./libre-translate-service.nix ];

            # Configuration Home Manager minimale pour l'utilisateur test.
            home.username = "test";
            home.homeDirectory = "/home/test";
         
            # Paquets nécessaires pour les paquets Emacs que l'on va utiliser.
            home.packages = with pkgs; [
              aspell
              grammalecte
              python3
	    ];

            # Fichier d'exemple.
            home.file = {
              ".Test.txt" = {
                text = builtins.readFile ./Test.txt;
                onChange = ''
                  cp /home/test/.Test.txt /home/test/Test.txt
                  chmod 0600 /home/test/Test.txt
                '';
              };
              ".Test-en.txt" = {
                text = builtins.readFile ./Test-en.txt;
                onChange = ''
                  cp /home/test/.Test-en.txt /home/test/Test-en.txt
                  chmod 0600 /home/test/Test-en.txt
                '';
              };
            };

            # Service de traduction Libre Translate lancé en tant que service systemd
	    # utilisateur.
            services.libre-translate = {
	      enable = true;
	      models = [ "translate-en_fr" "translate-fr_en" ];
	    };

            programs.emacs = let
	      # Notre configuration Emacs a besoin d'être adaptée aux chemins des outils
	      # que l'on utilise. On va lire un modèle de configuration puis substituer
	      # les valeurs dedans.
	      # Lecture du modèle :
              emacsConfigTemplate = builtins.readFile ./init.el;
	      # Remplacement des chemins.
              emacsConfig = lib.replaceStrings
                [ 
                  "@GRAMMALECTE_SITE@" 
                  "@GRAMMALECTE_PYTHON@" 
                ]
                [ 
                  "${pkgs.emacsPackages.flycheck-grammalecte}/"
                  "${grammalecte}/lib/python3.13/site-packages/grammalecte" 
                ]
                emacsConfigTemplate;

            in {
              enable = true;
	      # Installation des paquets Emacs nécessaires.
	      extraPackages = epkgs: with epkgs; [ flycheck flycheck-aspell flycheck-grammalecte flyspell-correct flyspell-correct-ivy guess-language gt ];
	      # Notre configuration.
	      extraConfig = emacsConfig;
            };

            # Lancer Emacs au démarrage de la session.
            xsession = {
              enable = false;
              windowManager.i3 = {
                enable = true;
                config = {
                  modifier = "Mod4";
                  bars = [ ];
                  startup = [
                    # Si vous voulez évoluer dans la VM : { command = "${pkgs.xterm}/bin/xterm"; notification = false; }
		    { command = "emacs --no-splash Test.txt"; notification = false; }
                  ];
                  window.border = 0;
                };
              };
            };

            home.stateVersion = "25.11";
          };
        }
      ];
    };

    # App pour lancer la VM avec nix run.
    apps.x86_64-linux.default = {
      type = "app";
      program = "${self.nixosConfigurations.vm.config.system.build.vm}/bin/run-emacs-vm-vm";
    };
  };
}

Le flake utilise un module libre-translate-service.nix mettant en place un service Libre Translate Systemd pour votre utilisateur. On peut lui indiquer les paires de langues à installer, ce qui évite de tout télécharger, vous pourrez modifier les paires de langues voulues dans votre configuration Home Manager. Libre Translate peut prendre beaucoup de place mémoire si on met beaucoup de langues ; pour ne pas gâcher toute cette mémoire lorsque vous ne l'utilisez pas, on se sert de l'activation de service Systemd sur demande: Libre Translate s'arrêtera tout seul au bout de deux minutes.

libre-translate-service.nix
# Module Home Manager pour Libre Translate avec modèles Argos.
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.services.libre-translate;
  
  # Import du package des modèles.
  argosModels = pkgs.callPackage ./argos-models-package.nix { };
  
  # Crée le package avec les modèles sélectionnés.
  modelsPackage = argosModels.mkModelsPackage cfg.models;
  
in {
  options.services.libre-translate = {
    enable = mkEnableOption "LibreTranslate service (user)";
    
    package = mkOption {
      type = types.package;
      default = pkgs.libretranslate;
      defaultText = literalExpression "pkgs.libretranslate";
      description = "The LibreTranslate package to use";
    };
    
    models = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "translate-fr_en" "translate-en_fr" "translate-es_en" ];
      description = ''
        Argos model codes to install.
        
        Available models are : ${concatStringsSep ", " argosModels.availableModels}
        
        To see all models, see argos-models.nix
      '';
    };
    
    host = mkOption {
      type = types.str;
      default = "127.0.0.1";
      description = "Host to bind the service to";
    };
    
    port = mkOption {
      type = types.port;
      default = 5000;
      description = "Port to bind the service to";
    };
    
    extraArgs = mkOption {
      type = types.listOf types.str;
      default = [];
      example = [ "--api-keys" "--require-api-key-origin" "--threads" "4" ];
      description = "Additional command-line arguments to pass to LibreTranslate";
    };
  };
  
  config = let
    cacheDir = "${config.home.homeDirectory}/.local/cache";
    argosCacheDir = "${config.home.homeDirectory}/.cache/argos-translate";
    dataDir = "${config.xdg.dataHome}";
  in
  mkIf cfg.enable {
    assertions = [
      {
        assertion = cfg.models != [];
        message = "services.libre-translate.models cannot be empty.";
      }
    ];
   
    # Libre Translate socket.
    systemd.user.sockets.libretranslate-proxy = {
      Unit = {
        Description = "LibreTranslate Proxy Socket";
      };
      Socket = {
        ListenStream = "127.0.0.1:${toString cfg.port}";
        Accept = false;
      };
      Install = {
        WantedBy = ["sockets.target"];
      };
    };
 
    systemd.user.services.libretranslate-proxy = {
      Unit = {
        Description = "LibreTranslate Proxy";
        Requires = [ "libretranslate-proxy.socket" "libretranslate.service" ];
        After = [ "libretranslate-proxy.socket" "libretranslate.service" ];
        BindsTo = [ "libretranslate.service" ];
      };
      Service = {
        Type = "notify";
        ExecStartPre = pkgs.writeShellScript "wait-for-libretranslate" ''
          for i in {1..30}; do
            if ${pkgs.netcat}/bin/nc -z 127.0.0.1 ${toString (cfg.port + 1)}; then
              # LibreTranslate is ready
              exit 0
            fi
            echo "Waiting for LibreTranslate... ($i/30)"
            sleep 2
          done
          echo "Timeout waiting for LibreTranslate"
          exit 1
        '';
        ExecStart = "${pkgs.systemd}/lib/systemd/systemd-socket-proxyd --exit-idle-time=120s  127.0.0.1:${toString (cfg.port + 1)}";
        PrivateTmp = true;
        TimeoutStopSec = "5s";
      };
      Install = {
        WantedBy = ["libretranslate.socket"];
      };
    };

    # Service systemd user.
    systemd.user.services.libretranslate = {
      Unit = {
        Description = "LibreTranslate Translation Service (user)";
        PartOf = [ "libretranslate-proxy.service" ];
        StopWhenUnneeded = true;
      };
      
      Service = {
        Type = "simple";
        Environment = [
          "HOME=%h"
          "LT_LOCAL_FILES=${dataDir}"
        ];
	WorkingDirectory = "${dataDir}";
        
        # Préparation des modèles avant le démarrage
        ExecStartPre = pkgs.writeShellScript "libretranslate-prestart" ''
          # Crée le répertoire pour les modèles
	  echo "Creating models dir: ${dataDir}/argos-translate/packages"
          ${pkgs.coreutils}/bin/mkdir -p ${dataDir}/argos-translate/packages
	  
          # Copie les modèles sélectionnés
	  for model in ${modelsPackage}/*.argosmodel ; do
            ${pkgs.unzip}/bin/unzip -o -d ${dataDir}/argos-translate/packages/ ''${model}
	  done
          
          echo "Models installed in ${dataDir}/argos-translate/packages/"
        '';
        
        ExecStart = ''
          ${cfg.package}/bin/libretranslate \
            --host ${cfg.host} \
            --port ${toString (cfg.port + 1)} \
            --load-only ${concatStringsSep "," cfg.models} \
            ${concatStringsSep " " cfg.extraArgs}
        '';
        TimeoutStopSec = "5s";
        
        Restart = "on-failure";
        
        # Sécurité (user service).
        NoNewPrivileges = true;
        PrivateTmp = true;
        ProtectSystem = "strict";
        ProtectHome = "read-only";
        
        # Permet l'écriture dans le cache et le répertoire de données.
        ReadWritePaths = [ argosCacheDir cacheDir dataDir ];
        
        # Limitations de ressources.
        LimitNOFILE = 65536;
      };
    };
    
    # Crée le répertoire de données et cache s'ils n'existent pas.
    home.activation.libretranslateSetup = lib.hm.dag.entryAfter ["writeBoundary"] ''
      $DRY_RUN_CMD mkdir -p ${dataDir}
      $DRY_RUN_CMD mkdir -p ${argosCacheDir}
      $DRY_RUN_CMD mkdir -p ${cacheDir}
    '';
  };
}

Les modèles de traduction des paires de langues sont disponibles sur le site des modèles Argos. Pour les intégrer à la configuration Nix, il faut créer des paquets à partir de cette liste. Le fichier prefetch-argos-models.py s'en charge.

prefetch-argos-models.py
#!/usr/bin/env python3
"""
Script pour préfetcher les modèles Argos et générer un fichier Nix
Usage: python3 prefetch-argos-models.py fr_en en_fr
"""

import json
import subprocess
import sys
from pathlib import Path
from urllib.request import urlopen

MODELS_INDEX="https://raw.githubusercontent.com/argosopentech/argospm-index/main/index.json"

def get_http_link(links):
    """Extrait le lien HTTP d'une liste de liens"""
    for link in links:
        if link.startswith("https://"):
            return link
    return None

def prefetch_url(url):
    """Lance nix-prefetch-url et retourne le hash"""
    try:
        result = subprocess.run(
            ["nix-prefetch-url", url],
            capture_output=True,
            text=True,
            check=True
        )
        return result.stdout.strip()
    except subprocess.CalledProcessError as e:
        print(f"Erreur lors du prefetch de {url}: {e}", file=sys.stderr)
        return None

def process_models(codes):
    """Traite le fichier JSON des models et génère les informations des modèles"""    
    with urlopen(MODELS_INDEX) as r:
        data = r.read().decode('utf-8')
        models = json.loads(data)
    
    results = []
    total = len(models)
    
    for idx, model in enumerate(models, 1):
        code = model['code']
        url = get_http_link(model['links'])

        if len(codes) != 0 and code not in codes:
            continue
        
        if not url:
            print(f"[{idx}/{total}] Aucun lien HTTP pour {code}, ignoré", file=sys.stderr)
            continue
        
        print(f"[{idx}/{total}] Prefetch de {code}...", file=sys.stderr)
        sha256 = prefetch_url(url)
        
        if sha256:
            results.append({
                'code': code,
                'from_code': model['from_code'],
                'to_code': model['to_code'],
                'from_name': model['from_name'],
                'to_name': model['to_name'],
                'version': model['package_version'],
                'url': url,
                'sha256': sha256
            })
            print(f"{code}: {sha256[:16]}...", file=sys.stderr)
        else:
            print(f"  ✗ Échec pour {code}", file=sys.stderr)
    
    return results

def generate_nix_expression(models):
    """Génère l'expression Nix pour les modèles"""
    nix_code = """# Modèles Argos pour LibreTranslate
# Généré automatiquement - ne pas modifier manuellement
{ fetchurl }:

{
"""
    
    for model in models:
        nix_code += f"""  "{model['code']}" = fetchurl {{
    url = "{model['url']}";
    sha256 = "{model['sha256']}";
  }};

"""
    
    nix_code += "}\n"
    return nix_code

def main():
    if len(sys.argv) < 1:
        print("Usage: python3 prefetch-argos-models.py models.json", file=sys.stderr)
        sys.exit(1)
    
    codes = ["translate-%s" % c for c in sys.argv[1:]]
    
    print("Traitement des modèles...", file=sys.stderr)
    models = process_models(codes)
    
    print(f"\n{len(models)} modèles traités avec succès", file=sys.stderr)
    
    # Génère l'expression Nix
    nix_expr = generate_nix_expression(models)
    
    # Écrit dans models.nix
    output_file = "argos-models.nix"
    with open(output_file, 'w') as f:
        f.write(nix_expr)
    
    print(f"\nFichier {output_file} généré!", file=sys.stderr)
    
    # Génère aussi un JSON avec les métadonnées
    with open("models-metadata.json", 'w') as f:
        json.dump(models, f, indent=2)
    
    print("Fichier models-metadata.json généré!", file=sys.stderr)

if __name__ == "__main__":
    main()

Le fichier argos-models-package.nix, lui, permet de créer un groupement des modèles que l'on souhaite utiliser sous forme de paquet Nix.

argos-models-package.nix
# Package pour les modèles Argos avec sélection modulaire.
{ lib, stdenv, fetchurl }:

let
  # Import des modèles disponibles.
  allModels = import ./argos-models.nix { inherit fetchurl; };

in
{
  # Fonction pour créer un répertoire avec les modèles sélectionnés.
  mkModelsPackage = modelCodes: stdenv.mkDerivation {
    name = "argos-translate-models";
    
    buildCommand = ''
      mkdir -p $out
      ${lib.concatMapStringsSep "\n" (code: 
        let
          model = allModels.${code} or (throw "Modèle '${code}' non trouvé. Modèles disponibles: ${lib.concatStringsSep ", " (lib.attrNames allModels)}");
        in
        ''
          cp ${model} $out/${code}.argosmodel
        ''
      ) modelCodes}
    '';
    
    meta = {
      description = "Modèles de traduction Argos sélectionnés";
      platforms = lib.platforms.all;
    };
  };
  
  # Liste de tous les modèles disponibles (pour documentation/validation)
  availableModels = lib.attrNames allModels;
  
  # Accès direct aux modèles individuels si besoin
  models = allModels;
}

Le fichier de test qu'Emacs va ouvrir dans la VM est le suivant.

Test.txt

Exemple d'erreur de grammaire. Vous pouvez corriger avec un clic droit 
ou bien en plaçant le curseur texte sur le mot et en appuyant sur "C-;"
(Maintenir la touche contrôle et appuyez sur ";").

   Les belettes et les loutre sont les mammifères les plus mignon.


Exemple d'erreur d'orthographe. Vous pouvez corriger de la même façon. Remarque: si vous
corrigez "céreale" par "céréale", une erreur de grammaire sera ensuite détectée.

Le ratton laveur mange des céreale qu'il a volé.


Pour traduire, sélectionnez le texte ci-dessous avec la souris ou bien "C-espace",
puis le raccourci clavier "C-c t".

La loutre et la belette sont parties ensemble au cinéma.


Pour changer de sens de traduction, effectuez une traduction du texte anglais ci dessous. 
Puis une fois le résultat affiché, vous pouvez appuyer sur "t" dans le tampon de réponse.

My dog ate my cornflakes.

Et un fichier en anglais est aussi disponible.

Test-en.txt

In english, no grammar error should be detected, because no grammar checker
is installed.

   
   Weasel and otter are the cutest mammals.


Example of spelling error. You can correct it the same way. Note: if you
correct "cereale" by "cereal", a grammar error will then be detected.

The raccoon eats cereales he stole.

To translate, select the text below with the mouse or "C-space",
then the keyboard shortcut "C-ct".

The otter and the weasel went together to the cinema.

Pour lancer la machine, tout d'abord choisir les pairs de langues souhaitées. Ici, on prend Anglais à Français et inversement.

python3 prefetch-argos-models.py en_fr fr_en

Si vous choisissez d'autres langues, n'oubliez pas de les renseigner dans le fichier flake.nix à cet endroit:

  services.libre-translate = {
	enable = true;
	models = [ "translate-en_fr" "translate-fr_en" ];
  };

Ainsi que dans le fichier init.el dans la variable my/langues-traduction:

(setq my/langues-traduction '(fr en))

Et ensuite la commande nix run lance la VM de test.

nix run

La VM se lance sur Emacs avec le fichier de test ouvert. Vous pouvez tester les différentes méthodes de correction et la traduction.

Correction d'une erreur d'orthographe avec la souris.
Correction d'une erreur d'orthographe avec la souris.
Correction d'une erreur de grammaire avec la souris.
Correction d'une erreur de grammaire avec la souris.
Correction d'une erreur d'orthographe avec le clavier, `C-;`.
Correction d'une erreur d'orthographe avec le clavier, `C-;`.
Traduction avec le clavier, `C-c t`.
Traduction avec le clavier, `C-c t`.

Installation dans votre configuration Home Manager

Téléchargez l'archive et recopiez les fichiers suivants dans votre configuration:

Ensuite, il faut récupérer les paires de langues disponibles pour le traducteur. Pour voir celles disponibles, rendez-vous sur la page des modèles Argos.

Dans l'exemple suivant, on prend Français vers Anglais et inversement :

python3 prefetch-argos-models.py en_fr fr_en

Le script crée un fichier Nix argos-models.nix avec les définitions Nix correspondantes.

Vous pouvez ensuite ajouter la configuration à votre configuration Home Manager.

D'abord des définitions pour Aspell et Grammalecte, à placer en début de votre configuration, ainsi que l'import du module mettant en place le service de traduction. Vous pouvez ajouter les dictionnaires dont vous avez besoin ici :

{ pkgs, ... }: 
let
  # Aspell, le correcteur d'orthographe. Avec des dictionnaires anglais et 
  # français pour l'example.
  aspell = (pkgs.aspellWithDicts (dicts: with dicts; [ en en-computers fr ]));

  # Grammalecte, le correcteur de grammaire.
  grammalecte = pkgs.python313Packages.grammalecte;
in {
  imports = [ ./libre-translate-service.nix ];

Dans vos paquets, ajoutez Aspell, Grammalecte et Python3 (Grammalecte a besoin de Python3.) :

  home.packages = with pkgs; [
    aspell
    grammalecte
    python3
  ];

Ensuite le service de traduction Libre Translate. Modifiez les langues suivant ce que vous voulez :

  # Service de traduction Libre Translate lancé en tant que service systemd
  # utilisateur.
  services.libre-translate = {
    enable = true;
    models = [ "translate-en_fr" "translate-fr_en" ];
  };

Et l'installation d'Emacs, avec les paquets nécessaires. Vous pouvez ajouter les paquets flyspell-correct-ivy ou flyspell-correct-helm si vous utilisez ces paquets Emacs :

  programs.emacs = let
    # Notre configuration Emacs a besoin d'être adaptée aux chemins des outils
    # que l'on utilise. On va lire un modèle de configuration puis substituer
    # les valeurs dedans.

    # Lecture du modèle:
    emacsConfigTemplate = builtins.readFile ./init.el;

    # Remplacement des chemins. Les valeurs sont le résultat de nombreux essais:
    emacsConfig = lib.replaceStrings
      [ 
        "@GRAMMALECTE_SITE@" 
        "@GRAMMALECTE_PYTHON@" 
      ]
      [ 
        "${pkgs.emacsPackages.flycheck-grammalecte}/"
        "${grammalecte}/lib/python3.13/site-packages/grammalecte" 
      ]
      emacsConfigTemplate;
  in {
    enable = true;

    # Installation des paquets Emacs nécessaires.
    extraPackages = epkgs: with epkgs; [ flycheck flycheck-aspell flycheck-grammalecte flyspell-correct gt guess-language ];
    # Notre configuration.
    extraConfig = emacsConfig;
  };

Modifiez les langues souhaitées dans le fichier init.el :

;;; On défini ici les langues avec lesquelles ont souhaite travailler.
;;; on associe le code langue (détecté par guess-language) au dictionnaire
;;; à utiliser et au nom de la langue.
;;; Mettez en premier la langue que vous souhaitez être par défaut si
;;; l'auto détection échoue.

(setq my/langues
  '(
    (fr . ("french" "Français"))
    (en . ("english" "English"))
  )
)

;;; Langues pour le traducteur
(setq my/langues-traduction '(fr en))

Si vous avez déjà une configuration Emacs, ajoutez là aussi dans le fichier init.el. Vous pouvez aussi en profiter pour ajouter des paquets flycheck-correct pour Ivy ou Helm, qui apportent plus de confort pour les corrections. Exemple pour Ivy :

(use-package flyspell-correct-ivy)

N'oubliez pas d'ajouter les fichiers à git :

git add libre-translate-service.nix prefetch-argos-models.py argos-models-package.nix init.el argos-models.nix

Ensuite, il ne vous reste plus qu'à déployer la configuration, comme d'habitude :

home-manager switch