Tradurre un'app Flutter con Ollama o LM Studio

Aggiornato il · LocalePilot

Un modello in esecuzione sul tuo computer può preparare bozze di traduzione per i tuoi file ARB, senza abbonamento e senza inviare il testo a un servizio online. Questa guida copre l'installazione, uno script Dart di partenza e i punti in cui il modello sbaglia.

Un modello locale non sostituisce un traduttore. Per le stringhe brevi di un'interfaccia, però, fornisce bozze decenti gratis, e il testo non lascia mai il tuo computer. Gran parte del lavoro sta attorno al modello: dargli contesto, proteggere i placeholder, revisionare quello che restituisce.

Installare un modello

Con Ollama, un solo comando scarica il modello, e un server locale resta in ascolto su http://localhost:11434:

ollama pull gemma3:12b

Un modello da 12 miliardi di parametri nella sua versione quantizzata predefinita sta in 16 GB di memoria. Su un computer meno potente, gemma3:4b gestisce le stringhe semplici, con più errori sulle frasi lunghe.

Con LM Studio, scegli il modello nell'app e avvii il server locale: espone un'API compatibile con OpenAI, su http://localhost:1234/v1 per impostazione predefinita.

Cosa deve sapere il modello

Inviare "Share list" da solo lascia il modello a indovinare se si tratta di un pulsante, di un titolo o di un verbo. Tre cose fanno una differenza netta:

  • La descrizione della chiave, se compili @key.description nei file ARB. È il motivo migliore per farlo.
  • Il contesto dell'app: "app di lista della spesa per la famiglia, tono semplice, si dà del tu".
  • Le regole di formato: mantenere intatti ogni {placeholder} e la sintassi ICU, rispondere solo con la traduzione.

Imposta anche la temperatura a 0: per una traduzione vuoi la risposta più probabile, non la varietà.

Uno script di partenza

Questo script Dart traduce le chiavi mancanti (o vuote) in app_fr.arb, una alla volta, con Ollama. Non tocca i file ARB: le bozze vanno in un file separato, e ogni risposta in cui i placeholder sono cambiati viene messa da parte.

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();
}

Aggiungi al prompt il contesto della tua app, e la lingua di destinazione se non è il francese (per esempio app_it.arb e "to Italian").

Dove sbaglia

Plurali ICU. È il punto debole. Un modello può tradurre la parola chiave other, perdere un ramo o rinominare {count} in un solo ramo. Il confronto dei placeholder dello script ne intercetta una parte; per il resto, revisiona ogni plurale. Attenzione anche ai rami =1, che in francese si applicano allo zero (dettagli nella guida sui plurali ICU).

Nomi propri. Il nome dell'app, il piano a pagamento, il nome di una funzionalità: i modelli tendono a tradurli. Elencali nel prompt come termini da mantenere, e controllali dopo.

Coerenza. Ogni stringa viene tradotta isolatamente, quindi "item" può diventare "articolo" qui ed "elemento" là. Un breve glossario nel prompt risolve gran parte del problema.

Lunghezza. Italiano, francese e tedesco sono spesso più lunghi dell'inglese. Un pulsante che ci stava in inglese può sforare; solo lo schermo te lo dirà.

Con LM Studio

Cambia solo la richiesta. L'API segue il formato OpenAI: invia una lista di messaggi a /v1/chat/completions e leggi choices[0].message.content dalla risposta. Il resto dello script resta invariato.

Revisionare, poi unire

Apri il file di bozze accanto ad app_en.arb e passalo chiave per chiave. Copia ciò che va bene in app_fr.arb, riesegui flutter gen-l10n e guarda le schermate interessate. È proprio questo ciclo che LocalePilot automatizza: i suggerimenti di Ollama o LM Studio arrivano nell'area di lavoro come "bozza IA da revisionare", la QA confronta i placeholder, e niente viene scritto nei file prima dell'esportazione.

Domande frequenti

Quale modello usare per le stringhe di un'app?

Un modello generalista recente da 12B come gemma3:12b dà buone bozze per stringhe brevi. Su un computer meno potente funziona una versione da 4B, con più errori.

Il mio testo lascia il computer?

No. Ollama e LM Studio eseguono il modello in locale; le richieste vanno a localhost.

Posso scrivere direttamente nei file ARB?

Meglio scrivere in un file di bozze e revisionare prima di unire: un modello può rompere un plurale ICU o tradurre il nome di un marchio.