Ein lokales Modell ersetzt keinen Übersetzer. Für kurze UI-Strings liefert es aber kostenlos brauchbare Entwürfe, und Ihr Text verlässt nie Ihren Rechner. Die meiste Arbeit liegt rund um das Modell: ihm Kontext geben, Platzhalter schützen, prüfen, was zurückkommt.
Ein Modell installieren
Mit Ollama lädt ein einziger Befehl das Modell herunter, und ein lokaler Server lauscht dann auf http://localhost:11434:
ollama pull gemma3:12b
Ein Modell mit 12 Milliarden Parametern passt in seiner standardmäßig quantisierten Fassung in 16 GB Arbeitsspeicher. Auf einem kleineren Rechner bewältigt gemma3:4b einfache Strings, mit mehr Fehlern bei langen Sätzen.
Mit LM Studio wählen Sie das Modell in der App und starten den lokalen Server: Er stellt eine OpenAI-kompatible API bereit, standardmäßig unter http://localhost:1234/v1.
Was das Modell wissen muss
Schickt man „Share list“ allein, muss das Modell raten, ob es ein Button, ein Titel oder ein Verb ist. Drei Dinge machen einen deutlichen Unterschied:
- Die Beschreibung des Schlüssels, wenn Sie
@key.descriptionin Ihren ARB-Dateien ausfüllen. Das ist der beste Grund, es zu tun. - Der Kontext der App: „Einkaufslisten-App für Familien, schlichter Ton, auf Deutsch duzen“.
- Formatregeln: jeden
{placeholder}und die ICU-Syntax unverändert lassen, nur mit der Übersetzung antworten.
Setzen Sie außerdem die Temperatur auf 0: Beim Übersetzen wollen Sie die wahrscheinlichste Antwort, keine Abwechslung.
Ein Skript für den Einstieg
Dieses Dart-Skript übersetzt die Schlüssel, die in app_fr.arb fehlen (oder leer sind), einen nach dem anderen, mit Ollama. Es rührt Ihre ARB-Dateien nicht an: Entwürfe landen in einer separaten Datei, und jede Antwort mit veränderten Platzhaltern wird beiseitegelegt.
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 changed, redo by hand');
continue;
}
drafts[key] = draft;
}
File('app_fr.drafts.json').writeAsStringSync(const JsonEncoder.withIndent(' ').convert(drafts));
print('${drafts.length} drafts in 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();
}
Ergänzen Sie den Prompt um den Kontext Ihrer App, und passen Sie Zielsprache und Dateinamen an, wenn Sie nicht ins Französische übersetzen (für Deutsch: app_de.arb und „to German“).
Wo es schiefgeht
ICU-Pluralformen. Das ist der Schwachpunkt. Ein Modell übersetzt vielleicht das Schlüsselwort other, lässt einen Zweig weg oder benennt {count} in nur einem Zweig um. Der Platzhaltervergleich des Skripts fängt einen Teil davon ab; für den Rest prüfen Sie jede Pluralform. Achten Sie auch auf =1-Zweige, die im Französischen für null gelten (Details in der Anleitung zu ICU-Pluralformen).
Eigennamen. Der Name Ihrer App, Ihr kostenpflichtiger Tarif, ein Funktionsname: Modelle übersetzen sie gern. Führen Sie sie im Prompt als beizubehaltende Begriffe auf, und prüfen Sie sie danach.
Konsistenz. Jeder String wird isoliert übersetzt, sodass „item“ hier zu „Artikel“ und dort zu „Element“ werden kann. Ein kurzes Glossar im Prompt behebt das meiste.
Länge. Deutsch und Französisch sind oft länger als Englisch. Ein Button, der auf Englisch passte, kann überlaufen; das zeigt Ihnen nur der Bildschirm.
Mit LM Studio
Nur die Anfrage ändert sich. Die API folgt dem OpenAI-Format: Senden Sie eine Liste von Nachrichten an /v1/chat/completions und lesen Sie choices[0].message.content aus der Antwort. Der Rest des Skripts bleibt gleich.
Prüfen, dann zusammenführen
Öffnen Sie die Entwurfsdatei neben app_en.arb und gehen Sie sie Schlüssel für Schlüssel durch. Kopieren Sie, was gut ist, in Ihre Zieldatei, führen Sie flutter gen-l10n erneut aus und sehen Sie sich die betroffenen Bildschirme an. Genau diesen Ablauf automatisiert LocalePilot: Vorschläge von Ollama oder LM Studio landen im Arbeitsbereich als „KI-Entwurf zum Prüfen“, die QA vergleicht die Platzhalter, und vor dem Export wird nichts in Ihre Dateien geschrieben.