Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions app/lib/core/i18n/hi/hi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/// Hindi translations for common app controls and connection/deck messages.
/// Missing entries intentionally fall back to English until the remaining strings
/// have been translated; source keys and placeholders must remain unchanged.
const Map<String, String> hiStrings = {
'Aparência': 'दिखावट',
'Automático': 'स्वचालित',
'Automático segue o idioma do aparelho.': 'स्वचालित विकल्प डिवाइस की भाषा का उपयोग करता है।',
'Restaurar': 'पुनर्स्थापित करें',
'Configurações': 'सेटिंग्स',
'Integrações': 'इंटीग्रेशन',
'Cancelar': 'रद्द करें',
'Salvar': 'सहेजें',
'Ação': 'क्रिया',
'Nenhuma': 'कोई नहीं',
'Parar': 'रोकें',
'Mutar': 'म्यूट करें',
'Gravando': 'रिकॉर्डिंग',
'Fonte': 'स्रोत',
'Novo botão': 'नया बटन',
'Nenhum perfil encontrado': 'कोई प्रोफ़ाइल नहीं मिली',
'Atualizar': 'अपडेट करें',
'Voltar': 'वापस',
'Pasta': 'फ़ोल्डर',
'Diagnóstico da conexão': 'कनेक्शन की जाँच',
'Fechar': 'बंद करें',
'Concluir edição': 'संपादन पूरा करें',
'Editar botões': 'बटन संपादित करें',
'Perfil': 'प्रोफ़ाइल',
'Excluir perfil': 'प्रोफ़ाइल हटाएँ',
'Novo perfil': 'नई प्रोफ़ाइल',
'Nenhum perfil selecionado': 'कोई प्रोफ़ाइल चयनित नहीं है',
'Nome do perfil': 'प्रोफ़ाइल का नाम',
'Criar': 'बनाएँ',
'Excluir': 'हटाएँ',
'Nome': 'नाम',
'Adicionar': 'जोड़ें',
'O PC não respondeu.': 'PC ने जवाब नहीं दिया।',
'Não deu certo: {error}': 'यह काम नहीं किया: {error}',
'Importar': 'आयात करें',
'Exportar backup': 'बैकअप निर्यात करें',
'Importar backup': 'बैकअप आयात करें',
'Editar botão': 'बटन संपादित करें',
'Rótulo': 'लेबल',
'Ícone': 'आइकन',
'Imagem própria': 'कस्टम छवि',
'Remover imagem': 'छवि हटाएँ',
'Cor de fundo': 'पृष्ठभूमि का रंग',
'Desligado': 'बंद',
'Ativo': 'सक्रिय',
'Opcional': 'वैकल्पिक',
'Imagem': 'छवि',
'Mostrar no botão': 'बटन पर दिखाएँ',
'Informação': 'जानकारी',
'Uso da CPU': 'CPU उपयोग',
'Uso da memória': 'मेमोरी उपयोग',
'Uso da GPU': 'GPU उपयोग',
'Rede': 'नेटवर्क',
'Relógio': 'घड़ी',
'Clima': 'मौसम',
'Comando': 'कमांड',
'URL': 'URL',
'Campo do JSON': 'JSON फ़ील्ड',
'Nada tocando': 'कुछ भी नहीं चल रहा',
'Contador': 'काउंटर',
'Cronômetro': 'स्टॉपवॉच',
'Pausado': 'रुका हुआ',
'Pausa': 'विराम',
'Foco': 'फ़ोकस',
'Fim!': 'समाप्त!',
'Timer': 'टाइमर',
'Zerar': 'रीसेट करें',
'Pular': 'छोड़ें',
'Próxima': 'अगला',
'Anterior': 'पिछला',
'Canal': 'चैनल',
'Sair': 'बाहर निकलें',
'Voz': 'आवाज़',
'Cena': 'दृश्य',
'Gravar': 'रिकॉर्ड करें',
'Ao vivo': 'लाइव',
'Microfone': 'माइक्रोफ़ोन',
'Saída de áudio': 'ऑडियो आउटपुट',
'Conectar': 'कनेक्ट करें',
'Chave': 'कुंजी',
'Esquecer': 'भूल जाएँ',
'Clássico': 'क्लासिक',
'Neon': 'नियॉन',
'OLED': 'OLED',
'Âmbar': 'एम्बर',
'Sólido': 'ठोस',
'Contorno': 'रूपरेखा',
'Leve': 'हल्का',
'Média': 'मध्यम',
'Forte': 'तेज़',
'Email': 'ईमेल',
'Senha': 'पासवर्ड',
'Conectado — {account}': 'कनेक्ट किया गया — {account}',
'Falha ao enviar o comando': 'कमांड भेजने में विफल',
};
23 changes: 13 additions & 10 deletions app/lib/core/i18n/i18n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,28 @@ import 'package:signals_flutter/signals_flutter.dart';

import 'en/en.dart';
import 'es/es.dart';
import 'hi/hi.dart';

/// Idioma da interface: o texto-fonte é português e [tr] devolve a tradução quando o
/// idioma é inglês ou espanhol. Texto sem tradução em espanhol cai no inglês; sem
/// tradução nenhuma aparece em português (nunca some).
///
/// Placeholders entre chaves: `tr('Conectado — {account}', {'account': nome})`.
/// Idioma da interface: o texto-fonte é português. Traduções ausentes em
/// espanhol ou hindi recorrem ao inglês; texto sem tradução aparece em português.
class AppLanguage {
AppLanguage._();

static const _prefKey = 'app_language';

/// Escolha do usuário: 'auto' (idioma do aparelho), 'pt', 'en' ou 'es'.
/// Escolha do usuário: 'auto' (idioma do aparelho), 'pt', 'en', 'es' ou 'hi'.
static final choice = signal('auto');

/// Idioma em uso: 'pt', 'en' ou 'es'.
/// Idioma em uso.
static final current = computed(() => resolve(choice.value));

static String resolve(String choice) {
if (choice == 'pt' || choice == 'en' || choice == 'es') return choice;
if (choice == 'pt' || choice == 'en' || choice == 'es' || choice == 'hi') {
return choice;
}
final device = PlatformDispatcher.instance.locale.languageCode;
return device == 'pt' || device == 'es' ? device : 'en';
if (device == 'pt' || device == 'es' || device == 'hi') return device;
return 'en';
}

static Future<void> load() async {
Expand All @@ -42,10 +43,11 @@ class AppLanguage {
} catch (_) {}
}

/// Valor do cabeçalho Accept-Language para o PC traduzir o que manda (schema, erros).
/// Valor do cabeçalho Accept-Language enviado ao PC.
static String get header => switch (current.value) {
'en' => 'en',
'es' => 'es',
'hi' => 'hi-IN',
_ => 'pt-BR',
};
}
Expand All @@ -54,6 +56,7 @@ String tr(String pt, [Map<String, Object?> args = const {}]) {
var s = switch (AppLanguage.current.value) {
'en' => enStrings[pt] ?? pt,
'es' => esStrings[pt] ?? enStrings[pt] ?? pt,
'hi' => hiStrings[pt] ?? enStrings[pt] ?? pt,
_ => pt,
};
args.forEach((k, v) => s = s.replaceAll('{$k}', '${v ?? ''}'));
Expand Down
1 change: 1 addition & 0 deletions app/lib/src/config/appearance_settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class AppearanceSettingsPage extends StatelessWidget {
const ButtonSegment(value: 'pt', label: Text('Português')),
const ButtonSegment(value: 'en', label: Text('English')),
const ButtonSegment(value: 'es', label: Text('Español')),
const ButtonSegment(value: 'hi', label: Text('हिन्दी')),
],
// Quatro opções: sem o ícone de marcado para caber em celular estreito.
showSelectedIcon: false,
Expand Down
11 changes: 11 additions & 0 deletions app/test/i18n_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ void main() {
expect(AppLanguage.header, 'en');
AppLanguage.choice.value = 'es';
expect(AppLanguage.header, 'es');
AppLanguage.choice.value = 'hi';
expect(AppLanguage.header, 'hi-IN');
});

test('Hindi is selectable and uses translated strings with English fallback', () {
AppLanguage.choice.value = 'hi';
expect(AppLanguage.resolve('hi'), 'hi');
expect(tr('Salvar'), 'सहेजें');
expect(tr('Texto que não existe'), 'Texto que não existe');
});

test('toda chave traduzida para inglês também tem espanhol', () {
Expand All @@ -34,6 +43,8 @@ void main() {
test('placeholders e texto sem tradução', () {
AppLanguage.choice.value = 'en';
expect(tr('Texto que não existe {x}', {'x': 1}), 'Texto que não existe 1');
AppLanguage.choice.value = 'hi';
expect(tr('Conectado — {account}', {'account': 'Aaqib'}), 'कनेक्ट किया गया — Aaqib');
});

test('toda tradução mantém os mesmos placeholders da chave', () {
Expand Down