Add localization support and translation script

- Introduced a new extension for localization in Flutter with `LocalizationExtension` in `l10n.dart`.
- Added a Python script `translate.py` for translating ARB/JSON localization files using a local Ollama model, preserving keys and placeholders, and handling ICU format rules.
This commit is contained in:
zjs81
2026-01-11 17:13:50 -07:00
parent 2495cd840f
commit b2ce82fe7e
64 changed files with 54716 additions and 1254 deletions
+25 -23
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../services/storage_service.dart';
import '../connector/meshcore_connector.dart';
@@ -181,7 +182,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Login failed: $e'),
content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red,
),
);
@@ -223,6 +224,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
@@ -235,7 +237,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Room Login'),
Text(l10n.login_roomLogin),
Text(
repeater.name,
style: TextStyle(
@@ -260,17 +262,17 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Enter the room password to access settings and status.',
style: TextStyle(fontSize: 14),
Text(
l10n.login_roomDescription,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'Password',
hintText: 'Enter password',
labelText: l10n.login_password,
hintText: l10n.login_enterPassword,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
@@ -297,13 +299,13 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
_savePassword = value ?? false;
});
},
title: const Text(
'Save password',
style: TextStyle(fontSize: 14),
title: Text(
l10n.login_savePassword,
style: const TextStyle(fontSize: 14),
),
subtitle: const Text(
'Password will be stored securely on this device',
style: TextStyle(fontSize: 12),
subtitle: Text(
l10n.login_savePasswordSubtitle,
style: const TextStyle(fontSize: 12),
),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
@@ -311,14 +313,14 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
const Divider(),
Row(
children: [
const Text(
'Routing',
style: TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
Text(
l10n.login_routing,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold),
),
const Spacer(),
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: 'Routing mode',
tooltip: l10n.login_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
@@ -334,7 +336,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
'Auto (use saved path)',
l10n.login_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
),
@@ -349,7 +351,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
const SizedBox(width: 8),
Text(
'Force Flood Mode',
l10n.login_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
),
@@ -372,7 +374,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
child: TextButton.icon(
onPressed: () => PathManagementDialog.show(context, contact: repeater),
icon: const Icon(Icons.timeline, size: 18),
label: const Text('Manage Paths'),
label: Text(l10n.login_managePaths),
),
),
],
@@ -380,7 +382,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
child: Text(l10n.common_cancel),
),
if (_isLoggingIn)
SizedBox(
@@ -399,7 +401,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
),
),
const SizedBox(width: 12),
Text('Attempt $_currentAttempt/$_maxAttempts'),
Text(l10n.login_attempt(_currentAttempt, _maxAttempts)),
],
),
),
@@ -408,7 +410,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
FilledButton.icon(
onPressed: _isLoading ? null : _handleLogin,
icon: const Icon(Icons.login, size: 18),
label: const Text('Login'),
label: Text(l10n.login_login),
),
],
);