Traduire une app Flutter avec Ollama ou LM Studio
Mis à jour le · GLabs325
Un modèle qui tourne sur votre machine peut produire des brouillons de traduction pour vos fichiers ARB, sans abonnement et sans envoyer vos textes à un service en ligne. La mise en place, un script Dart de départ, et les endroits où le modèle se trompe.
Un modèle local ne remplace pas un traducteur. Pour des libellés d'interface courts, il donne en revanche des brouillons corrects, gratuitement, sans que vos textes sortent de la machine. Le travail se situe surtout autour : donner du contexte, protéger les placeholders, relire.
Installer un modèle
Avec Ollama, une commande télécharge le modèle, et un serveur local écoute ensuite sur http://localhost:11434 :
ollama pull gemma3:12b
Un modèle de 12 milliards de paramètres dans sa version quantifiée par défaut tient dans 16 Go de mémoire. Sur une machine plus modeste, gemma3:4b fait l'affaire pour des chaînes simples, avec plus d'erreurs sur les phrases longues.
Avec LM Studio, on choisit le modèle dans l'interface, puis on démarre le serveur local : il expose une API compatible OpenAI, par défaut sur http://localhost:1234/v1.
Ce que le modèle doit savoir
Envoyer « Share list » seul, c'est laisser le modèle deviner s'il s'agit d'un bouton, d'un titre ou d'un verbe. Trois informations changent nettement la qualité :
- La description de la clé, si vous remplissez
@clé.descriptiondans vos ARB. C'est la meilleure raison de le faire. - Le contexte de l'app : « liste de courses partagée en famille, ton simple, tutoiement ».
- Les consignes de forme : garder chaque
{placeholder}et la syntaxe ICU intacts, répondre avec la traduction seule.
Mettez aussi la température à 0 : pour de la traduction, on veut la réponse la plus probable, pas de la variété.
Un script de départ
Ce script Dart traduit les clés absentes (ou vides) de app_fr.arb, une par une, avec Ollama. Il n'écrit pas dans vos ARB : les brouillons partent dans un fichier à part, et toute réponse dont les placeholders ont changé est écartée.
import 'dart:convert';
import 'dart:io';
const model = 'gemma3:12b';
final placeholder = RegExp(r'\{[A-Za-z_]\w*\}');
Future<void> main() async {
final source = _read('lib/l10n/app_en.arb');
final target = _read('lib/l10n/app_fr.arb');
final drafts = <String, String>{};
for (final key in source.keys.where((k) => !k.startsWith('@'))) {
if ((target[key] as String?)?.isNotEmpty ?? false) continue;
final text = source[key] as String;
final description = (source['@$key'] as Map?)?['description'] ?? '';
final draft = await _translate(text, description as String);
if (_placeholders(draft) != _placeholders(text)) {
stderr.writeln('$key : placeholders modifiés, à reprendre à la main');
continue;
}
drafts[key] = draft;
}
File('app_fr.drafts.json').writeAsStringSync(const JsonEncoder.withIndent(' ').convert(drafts));
print('${drafts.length} brouillons dans app_fr.drafts.json');
}
Map<String, dynamic> _read(String path) => jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>;
String _placeholders(String text) => (placeholder.allMatches(text).map((m) => m[0]!).toList()..sort()).join(',');
Future<String> _translate(String text, String description) async {
final client = HttpClient();
final request = await client.postUrl(Uri.parse('http://localhost:11434/api/generate'));
request.headers.contentType = ContentType.json;
request.write(jsonEncode({
'model': model,
'stream': false,
'options': {'temperature': 0},
'prompt': 'Translate this mobile app string from English to French. '
'Keep every {placeholder} and the ICU plural/select syntax unchanged. '
'Context: $description\n'
'Reply with the translation only.\n\n$text',
}));
final response = await request.close();
final body = jsonDecode(await response.transform(utf8.decoder).join()) as Map<String, dynamic>;
client.close();
return (body['response'] as String).trim();
}
Le prompt est en anglais : beaucoup de modèles suivent mieux les consignes dans cette langue, même quand ils traduisent vers le français. Ajoutez-y le contexte de votre app.
Là où ça coince
Les pluriels ICU. C'est le point faible. Un modèle peut traduire le mot-clé other, supprimer une branche ou réécrire {count} dans une seule branche. La comparaison des placeholders du script en attrape une partie ; pour le reste, relisez chaque pluriel. Attention aussi à la branche =1, qui s'applique à zéro en français (détails dans le guide sur les pluriels ICU).
Les noms propres. Nom de l'app, de l'offre payante, d'une fonctionnalité : un modèle a tendance à les traduire. Listez-les dans le prompt comme termes à ne pas traduire, et vérifiez-les ensuite.
La cohérence. Chaque chaîne est traduite isolément : « item » peut devenir « article » ici et « élément » là. Un glossaire de quelques termes dans le prompt règle l'essentiel.
La longueur. Le français est souvent plus long que l'anglais. Un bouton qui tenait en anglais peut déborder ; seul l'écran le dira.
Avec LM Studio
Seul l'appel change. L'API suit le format OpenAI : on envoie une liste de messages à /v1/chat/completions et on lit choices[0].message.content dans la réponse. Le reste du script ne bouge pas.
Relire, puis intégrer
Ouvrez le fichier de brouillons à côté de app_en.arb et relisez clé par clé. Copiez ensuite ce qui est bon dans app_fr.arb, relancez flutter gen-l10n, et regardez les écrans concernés. C'est ce circuit que LocalePilot automatise : les propositions d'Ollama ou de LM Studio arrivent dans l'espace de travail en « Brouillon IA à relire », la QA compare les placeholders, et rien n'est écrit dans vos fichiers avant l'export.
Questions fréquentes
Quel modèle choisir pour traduire des libellés d'app ?
Un modèle généraliste récent de 12 milliards de paramètres, comme gemma3:12b, donne de bons brouillons pour des chaînes courtes. Sur une machine plus modeste, une version 4b fonctionne avec plus d'erreurs.
Mes textes quittent-ils ma machine ?
Non. Ollama et LM Studio exécutent le modèle localement ; les requêtes vont vers localhost.
Peut-on écrire directement dans les fichiers ARB ?
Mieux vaut écrire dans un fichier de brouillons et relire avant d'intégrer : un modèle peut casser un pluriel ICU ou traduire un nom de marque.