From 87256e2c74b40ef28544b73e0ad00282d8d45e89 Mon Sep 17 00:00:00 2001 From: stopflock Date: Mon, 29 Sep 2025 11:56:28 -0500 Subject: [PATCH] Bump version, dynamic localizations, add portuguese --- lib/dev_config.dart | 2 +- lib/localizations/pt.json | 258 +++++++++++++++++++++++++ lib/services/localization_service.dart | 41 +++- 3 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 lib/localizations/pt.json diff --git a/lib/dev_config.dart b/lib/dev_config.dart index db1446c..843bfa9 100644 --- a/lib/dev_config.dart +++ b/lib/dev_config.dart @@ -31,7 +31,7 @@ const double kAddPinYOffset = 0.0; // Client name and version for OSM uploads ("created_by" tag) const String kClientName = 'DeFlock'; -const String kClientVersion = '0.9.12'; +const String kClientVersion = '0.9.13'; // Development/testing features - set to false for production builds const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json new file mode 100644 index 0000000..3ba76e9 --- /dev/null +++ b/lib/localizations/pt.json @@ -0,0 +1,258 @@ +{ + "language": { + "name": "Português" + }, + "app": { + "title": "DeFlock" + }, + "actions": { + "tagNode": "Novo Nó", + "download": "Baixar", + "settings": "Configurações", + "edit": "Editar", + "delete": "Excluir", + "cancel": "Cancelar", + "ok": "OK", + "close": "Fechar", + "submit": "Enviar", + "saveEdit": "Salvar Edição", + "clear": "Limpar" + }, + "followMe": { + "off": "Ativar seguir-me (norte para cima)", + "northUp": "Ativar seguir-me (rotação)", + "rotating": "Desativar seguir-me" + }, + "settings": { + "title": "Configurações", + "language": "Idioma", + "systemDefault": "Padrão do Sistema", + "aboutInfo": "Sobre / Informações", + "aboutThisApp": "Sobre este App", + "maxNodes": "Máx. de nós obtidos/desenhados", + "maxNodesSubtitle": "Definir um limite superior para o número de nós no mapa (padrão: 250).", + "maxNodesWarning": "Você provavelmente não quer fazer isso a menos que tenha certeza absoluta de que tem uma boa razão para isso.", + "offlineMode": "Modo Offline", + "offlineModeSubtitle": "Desabilitar todas as requisições de rede exceto para áreas locais/offline.", + "offlineModeWarningTitle": "Downloads Ativos", + "offlineModeWarningMessage": "Ativar o modo offline cancelará qualquer download de área ativo. Deseja continuar?", + "enableOfflineMode": "Ativar Modo Offline" + }, + "node": { + "title": "Nó #{}", + "tagSheetTitle": "Tags do Dispositivo de Vigilância", + "queuedForUpload": "Nó na fila para envio", + "editQueuedForUpload": "Edição de nó na fila para envio", + "deleteQueuedForUpload": "Exclusão de nó na fila para envio", + "confirmDeleteTitle": "Excluir Nó", + "confirmDeleteMessage": "Tem certeza de que deseja excluir o nó #{}? Esta ação não pode ser desfeita." + }, + "addNode": { + "profile": "Perfil", + "direction": "Direção {}°", + "profileNoDirectionInfo": "Este perfil não requer uma direção.", + "mustBeLoggedIn": "Você deve estar logado para enviar novos nós. Por favor, faça login via Configurações.", + "enableSubmittableProfile": "Ative um perfil enviável nas Configurações para enviar novos nós.", + "profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para enviar novos nós.", + "refineTags": "Refinar Tags", + "refineTagsWithProfile": "Refinar Tags ({})" + }, + "editNode": { + "title": "Editar Nó #{}", + "profile": "Perfil", + "direction": "Direção {}°", + "profileNoDirectionInfo": "Este perfil não requer uma direção.", + "mustBeLoggedIn": "Você deve estar logado para editar nós. Por favor, faça login via Configurações.", + "sandboxModeWarning": "Não é possível enviar edições de nós de produção para o sandbox. Mude para o modo Produção nas Configurações para editar nós.", + "enableSubmittableProfile": "Ative um perfil enviável nas Configurações para editar nós.", + "profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para editar nós.", + "refineTags": "Refinar Tags", + "refineTagsWithProfile": "Refinar Tags ({})" + }, + "download": { + "title": "Baixar Área do Mapa", + "maxZoomLevel": "Nível máx. de zoom", + "storageEstimate": "Estimativa de armazenamento:", + "tilesAndSize": "{} tiles, {} MB", + "minZoom": "Zoom mín.:", + "maxRecommendedZoom": "Zoom máx. recomendado: Z{}", + "withinTileLimit": "Dentro do limite de {} tiles", + "exceedsTileLimit": "A seleção atual excede o limite de {} tiles", + "offlineModeWarning": "Downloads desabilitados no modo offline. Desative o modo offline para baixar novas áreas.", + "downloadStarted": "Download iniciado! Buscando tiles e câmeras...", + "downloadFailed": "Falha ao iniciar o download: {}" + }, + "uploadMode": { + "title": "Destino do Upload", + "subtitle": "Escolha onde as câmeras são enviadas", + "production": "Produção", + "sandbox": "Sandbox", + "simulate": "Simular", + "productionDescription": "Enviar para o banco de dados OSM ao vivo (visível para todos os usuários)", + "sandboxDescription": "Uploads vão para o Sandbox OSM (seguro para testes, redefine regularmente).", + "simulateDescription": "Simular uploads (não contacta servidores OSM)" + }, + "auth": { + "loggedInAs": "Logado como {}", + "loginToOSM": "Fazer login no OpenStreetMap", + "tapToLogout": "Toque para sair", + "requiredToSubmit": "Necessário para enviar dados de câmeras", + "loggedOut": "Deslogado", + "testConnection": "Testar Conexão", + "testConnectionSubtitle": "Verificar se as credenciais OSM estão funcionando", + "connectionOK": "Conexão OK - credenciais são válidas", + "connectionFailed": "Conexão falhou - por favor, faça login novamente" + }, + "queue": { + "pendingUploads": "Uploads pendentes: {}", + "simulateModeEnabled": "Modo simulação ativado – uploads simulados", + "sandboxMode": "Modo sandbox – uploads vão para o Sandbox OSM", + "tapToViewQueue": "Toque para ver a fila", + "clearUploadQueue": "Limpar Fila de Upload", + "removeAllPending": "Remover todos os {} uploads pendentes", + "clearQueueTitle": "Limpar Fila", + "clearQueueConfirm": "Remover todos os {} uploads pendentes?", + "queueCleared": "Fila limpa", + "uploadQueueTitle": "Fila de Upload ({} itens)", + "queueIsEmpty": "A fila está vazia", + "cameraWithIndex": "Câmera {}", + "error": " (Erro)", + "completing": " (Completando...)", + "destination": "Dest: {}", + "latitude": "Lat: {}", + "longitude": "Lon: {}", + "direction": "Direção: {}°", + "attempts": "Tentativas: {}", + "uploadFailedRetry": "Upload falhou. Toque em tentar novamente para tentar novamente.", + "retryUpload": "Tentar upload novamente", + "clearAll": "Limpar Tudo" + }, + "tileProviders": { + "title": "Provedores de Tiles", + "noProvidersConfigured": "Nenhum provedor de tiles configurado", + "tileTypesCount": "{} tipos de tiles", + "apiKeyConfigured": "Chave API configurada", + "needsApiKey": "Precisa de chave API", + "editProvider": "Editar Provedor", + "addProvider": "Adicionar Provedor", + "deleteProvider": "Excluir Provedor", + "deleteProviderConfirm": "Tem certeza de que deseja excluir \"{}\"?", + "providerName": "Nome do Provedor", + "providerNameHint": "ex., Mapas Personalizados Inc.", + "providerNameRequired": "Nome do provedor é obrigatório", + "apiKey": "Chave API (Opcional)", + "apiKeyHint": "Insira a chave API se necessária pelos tipos de tiles", + "tileTypes": "Tipos de Tiles", + "addType": "Adicionar Tipo", + "noTileTypesConfigured": "Nenhum tipo de tile configurado", + "atLeastOneTileTypeRequired": "Pelo menos um tipo de tile é obrigatório", + "manageTileProviders": "Gerenciar Provedores" + }, + "tileTypeEditor": { + "editTileType": "Editar Tipo de Tile", + "addTileType": "Adicionar Tipo de Tile", + "name": "Nome", + "nameHint": "ex., Satélite", + "nameRequired": "Nome é obrigatório", + "urlTemplate": "Modelo de URL", + "urlTemplateHint": "https://exemplo.com/{z}/{x}/{y}.png", + "urlTemplateRequired": "Modelo de URL é obrigatório", + "urlTemplatePlaceholders": "URL deve conter os marcadores {z}, {x} e {y}", + "attribution": "Atribuição", + "attributionHint": "© Provedor de Mapas", + "attributionRequired": "Atribuição é obrigatória", + "fetchPreview": "Buscar Preview", + "previewTileLoaded": "Tile de preview carregado com sucesso", + "previewTileFailed": "Falha ao buscar preview: {}", + "save": "Salvar" + }, + "profiles": { + "nodeProfiles": "Perfis de Nó", + "newProfile": "Novo Perfil", + "builtIn": "Integrado", + "custom": "Personalizado", + "view": "Ver", + "deleteProfile": "Excluir Perfil", + "deleteProfileConfirm": "Tem certeza de que deseja excluir \"{}\"?", + "profileDeleted": "Perfil excluído" + }, + "mapTiles": { + "title": "Tiles do Mapa", + "manageProviders": "Gerenciar Provedores" + }, + "profileEditor": { + "viewProfile": "Ver Perfil", + "newProfile": "Novo Perfil", + "editProfile": "Editar Perfil", + "profileName": "Nome do perfil", + "profileNameHint": "ex., Câmera ALPR Personalizada", + "profileNameRequired": "Nome do perfil é obrigatório", + "requiresDirection": "Requer Direção", + "requiresDirectionSubtitle": "Se câmeras deste tipo precisam de uma tag de direção", + "submittable": "Enviável", + "submittableSubtitle": "Se este perfil pode ser usado para envios de câmeras", + "osmTags": "Tags OSM", + "addTag": "Adicionar Tag", + "saveProfile": "Salvar Perfil", + "keyHint": "chave", + "valueHint": "valor", + "atLeastOneTagRequired": "Pelo menos uma tag é obrigatória", + "profileSaved": "Perfil \"{}\" salvo" + }, + "operatorProfileEditor": { + "newOperatorProfile": "Novo Perfil de Operador", + "editOperatorProfile": "Editar Perfil de Operador", + "operatorName": "Nome do operador", + "operatorNameHint": "ex., Departamento de Polícia de Austin", + "operatorNameRequired": "Nome do operador é obrigatório", + "operatorProfileSaved": "Perfil de operador \"{}\" salvo" + }, + "operatorProfiles": { + "title": "Perfis de Operador", + "noProfilesMessage": "Nenhum perfil de operador definido. Crie um para aplicar tags de operador aos envios de nós.", + "tagsCount": "{} tags", + "deleteOperatorProfile": "Excluir Perfil de Operador", + "deleteOperatorProfileConfirm": "Tem certeza de que deseja excluir \"{}\"?", + "operatorProfileDeleted": "Perfil de operador excluído" + }, + "offlineAreas": { + "noAreasTitle": "Nenhuma área offline", + "noAreasSubtitle": "Baixe uma área do mapa para uso offline.", + "provider": "Provedor", + "maxZoom": "Zoom máx", + "zoomLevels": "Z{}-{}", + "latitude": "Lat", + "longitude": "Lon", + "tiles": "Tiles", + "size": "Tamanho", + "cameras": "Câmeras", + "areaIdFallback": "Área {}...", + "renameArea": "Renomear área", + "refreshWorldTiles": "Atualizar/rebaixar tiles mundiais", + "deleteOfflineArea": "Excluir área offline", + "cancelDownload": "Cancelar download", + "renameAreaDialogTitle": "Renomear Área Offline", + "areaNameLabel": "Nome da Área", + "renameButton": "Renomear", + "megabytes": "MB", + "kilobytes": "KB", + "progress": "{}%" + }, + "refineTagsSheet": { + "title": "Refinar Tags", + "operatorProfile": "Perfil de Operador", + "done": "Concluído", + "none": "Nenhum", + "noAdditionalOperatorTags": "Nenhuma tag adicional de operador", + "additionalTags": "tags adicionais", + "additionalTagsTitle": "Tags Adicionais", + "noTagsDefinedForProfile": "Nenhuma tag definida para este perfil de operador.", + "noOperatorProfiles": "Nenhum perfil de operador definido", + "noOperatorProfilesMessage": "Crie perfis de operador nas Configurações para aplicar tags adicionais aos seus envios de nós." + }, + "layerSelector": { + "cannotChangeTileTypes": "Não é possível alterar tipos de tiles durante o download de áreas offline", + "selectMapLayer": "Selecionar Camada do Mapa", + "noTileProvidersAvailable": "Nenhum provedor de tiles disponível" + } +} \ No newline at end of file diff --git a/lib/services/localization_service.dart b/lib/services/localization_service.dart index 78fc77f..41bf904 100644 --- a/lib/services/localization_service.dart +++ b/lib/services/localization_service.dart @@ -24,9 +24,44 @@ class LocalizationService extends ChangeNotifier { } Future _discoverAvailableLanguages() async { - // For now, we'll hardcode the languages we support - // In the future, this could scan the assets directory - _availableLanguages = ['en', 'es', 'fr', 'de']; + _availableLanguages = []; + + try { + // Get the asset manifest to find all localization files + final manifestContent = await rootBundle.loadString('AssetManifest.json'); + final Map manifestMap = json.decode(manifestContent); + + // Find all .json files in lib/localizations/ + final localizationFiles = manifestMap.keys + .where((String key) => key.startsWith('lib/localizations/') && key.endsWith('.json')) + .toList(); + + for (final filePath in localizationFiles) { + // Extract language code from filename (e.g., 'lib/localizations/pt.json' -> 'pt') + final fileName = filePath.split('/').last; + final languageCode = fileName.substring(0, fileName.length - 5); // Remove '.json' + + try { + // Try to load and parse the file to ensure it's valid + final jsonString = await rootBundle.loadString(filePath); + final parsedJson = json.decode(jsonString); + + // Basic validation - ensure it has the expected structure + if (parsedJson is Map && parsedJson.containsKey('language')) { + _availableLanguages.add(languageCode); + debugPrint('Found localization: $languageCode'); + } + } catch (e) { + debugPrint('Failed to load localization file $filePath: $e'); + } + } + } catch (e) { + debugPrint('Failed to read AssetManifest.json: $e'); + // If manifest reading fails, we'll have an empty list + // The system will handle this gracefully by falling back to 'en' in _loadSavedLanguage + } + + debugPrint('Available languages: $_availableLanguages'); } Future _loadSavedLanguage() async {