OpenCADIrrigation Abrir o app

Idiomas e site

A interface está em 20 idiomas, os mais falados do mundo. Português, inglês e espanhol vêm compilados no app; chinês (zh), hindi (hi), árabe (ar), francês (fr), bengali (bn), russo (ru), urdu (ur), indonésio (id), alemão (de), japonês (ja), marati (mr), vietnamita (vi), télugo (te), hauçá (ha), turco (tr), panjabi (pa, escrita gurmukhi) e suaíli (sw) são pacotes de idioma em assets/i18n/<código>.json, baixados só quando o idioma é escolhido (a interface mostra inglês até o pacote chegar, uma fração de segundo). Cada pacote traduz toda a tabela de textos, o tutorial interativo e os nomes do catálogo padrão; o worker de projeto recebe o pacote junto com o pedido, então relatórios e plotagens também saem no idioma escolhido. O app abre no primeiro idioma da lista de idiomas preferidos do sistema (do navegador ou do sistema operacional) que ele fala, e em inglês quando nenhum deles é falado; a troca em Ver → Idioma (cada idioma com o próprio nome) fica salva. As páginas de idioma (/pt/, /de/, …) e o parâmetro ?lang= valem só para a visita. O atributo lang do documento acompanha o idioma do app (pt-BR, zh-CN, senão o código). Em árabe e urdu a interface é montada da direita para a esquerda. Os PDFs imprimem todas essas escritas (Relatórios, plotagem e exportação): antes de gravar um PDF, as fontes Noto de que os seus textos precisam são carregadas uma vez por sessão. Os projetos de exemplo distribuídos existem nos 20 idiomas (examples/<código>/).

Um pacote é um JSON simples com três seções: strings (chave → texto), tutorial e catalog (texto de origem em inglês → texto). Tradutores podem revisá-lo sem tocar no código, e falantes nativos o revisam numa planilha (abaixo); test/core/i18n_packs_test.dart verifica que cada pacote cobre todas as chaves com os mesmos marcadores.

O site é gerado por dart run tool/gen_site.dart a partir de tool/site.dart: web/index.html (inglês, a página de quem chega sem idioma escolhido), um web/<código>/index.html para cada um dos outros 19 idiomas da interface (web/pt/, web/es/, web/de/, web/zh/, …; a lista segue supportedLanguages, então um idioma novo no app precisa da sua página), um redirecionamento em web/en/index.html para o endereço que o inglês tinha antes, web/sitemap.xml, web/404.html (no idioma do navegador quando o site o tem), a política de privacidade e os termos de uso de cada idioma (web/privacy/, web/<código>/privacy/, e terms/ do mesmo jeito; escritos em português, inglês e espanhol, em inglês nos demais até serem traduzidos, e então fora da indexação), um manifest por idioma (web/manifest.json, web/<código>/manifest.json: a mesma identidade do app, endereço de início, nomes e atalhos naquele idioma, uma captura larga e uma estreita), web/site.js (o único script das páginas: inicia o app quando ele é pedido e pergunta o consentimento das estatísticas) e a configuração de hospedagem hosting/firebase.json (veja hosting/README.md). Os textos das páginas ficam em tool/site.dart (português, inglês, espanhol) e em tool/site/<código>.dart (os demais, com a galeria de exemplos tirada dos pacotes de idioma); as páginas em árabe e urdu são da direita para a esquerda. Como o Flutter desenha em canvas, cada página traz o conteúdo em HTML real (título, recursos, exemplos, perguntas frequentes), com canônica própria, hreflang entre todas as versões (também os genéricos pt e zh ao lado de pt-BR e zh-CN), uma Content Security Policy (scripts só do site, WebAssembly para o CanvasKit, nenhum script inline, nenhum plugin nem frame), Open Graph (com uma imagem de compartilhamento na escrita da própria página, web/og/og-image-<código>.jpg, gerada a partir da página por node tool/og/render_og.js; o inglês mantém web/og-image.png) e JSON-LD (aplicativo, perguntas frequentes, como funciona e galeria de exemplos, no idioma da página, com o assunto ligado à Wikipédia/Wikidata). O teste test/web_metadata_test.dart falha quando os arquivos gerados divergem do template.

Esse HTML é tudo o que uma primeira visita baixa (e o que buscadores e o PageSpeed medem): o app, de vários megabytes, só é baixado quando o visitante o abre (os botões e os links de exemplo da página o iniciam ali mesmo), chega por um endereço do app (?example=, ?tutorial=, ?design=, ?ai=, ?lang=, o ?source=pwa do app instalado) ou já o usou naquele navegador (qualquer chave opencadirrigation. no localStorage); a página fica então escondida sob o app. web/flutter_bootstrap.js é o carregador do Flutter sem o service worker obsoleto do Flutter, e tool/build_web.sh tira a referência ao flutter.js.map que o build não gera e compila com --no-web-resources-cdn: o CanvasKit e a fonte Roboto vêm do próprio site, não da CDN do Google. O logo do topo é WebP em três larguras (web/brand/logo-horizontal-{520,780,1040}.webp, feitos de logo-horizontal.png), com o endereço versionado por um hash do arquivo. O site é servido pelo Firebase Hosting a partir de uma pasta de publicação fora deste repositório (public/, cópia de build/web, ao lado do firebase.json); a configuração é versionada em hosting/firebase.json, gerada com as páginas (hosting/README.md diz como aplicá-la). Ela não tem rewrite genérico, então um endereço inexistente recebe o 404.html com status 404 em vez da página do app (o que fazia o PageSpeed ver /.well-known/ai-catalog.json como um catálogo de IA quebrado); todo arquivo recebe nosniff, SAMEORIGIN, uma Referrer-Policy e uma Permissions-Policy limitada à posição do aparelho e ao microfone; as páginas, os manifests e todo arquivo do app cujo nome não muda de build para build (main.dart.js, o worker, flutter_bootstrap.js, sw.js, offline.js, site.js, assets/**, canvaskit/**) vão com no-cache, as imagens de brand/, icons/, screenshots/ e da raiz ficam 30 dias em cache, e /en/ responde com um 301 para /.

Abertura sem rede. Quando o primeiro quadro já está na tela e o navegador está ocioso, flutter_bootstrap.js carrega web/offline.js, que registra o service worker do próprio app, web/sw.js; assim a primeira visita nunca fica mais lenta por causa dele. tool/build_web.sh termina com dart run tool/gen_sw.dart, que grava no sw.js do build o nome do build (versão, commit git, data e um hash dos arquivos) e os arquivos de que uma abertura pode precisar, com seus SHA-256: a página raiz, o bootstrap, main.dart.js, o worker de cálculo, site.js, o manifest e os ícones, os assets do Flutter, o logo e o CanvasKit (só a variante do navegador), cerca de 17 MB descompactados. Os pacotes de idioma, as fontes do PDF e a lista de licenças também vão listados com o hash, mas só são guardados quando uma abertura os usa, e um build novo leva adiante os que o navegador já guardou, baixando a versão nova quando mudaram. Na instalação o worker preenche um cache por build (oci-shell-<build>): os arquivos que não mudaram desde o build anterior são copiados do cache dele, os outros são baixados passando por cima do cache HTTP e conferidos pelo hash. Ele responde primeiro pelo cache e revalida a página de abertura depois do primeiro quadro; guarda as fontes de fallback que o motor baixa de fonts/ assim que são usadas (o endereço delas leva a versão, então uma cópia serve a todos os builds); tiles de mapa, elevação, clima e analytics vão direto para a rede. Um build novo é instalado em segundo plano; o offline.js avisa o app, que oferece recarregar nele (Há uma versão nova do OpenCADIrrigation pronta, depois de avisar do trabalho não salvo, que vai para o autossalvamento), e senão ele é assumido na próxima abertura. Uma página já aberta continua com o build com que abriu (as outras páginas abertas mantêm o delas quando uma recarrega), uma página aberta por dias procura um build novo a cada hora e quando volta à tela, e o cache de um build é apagado quando nenhuma página o usa mais. Um sw.js que não foi preenchido (flutter run, um flutter build web simples) só remove a si mesmo e os seus caches.

Fontes servidas pelo site. O Flutter traz o Roboto (latim, cirílico, grego); os glifos de todas as outras escritas e os símbolos vêm das fontes de reserva do motor, que ele baixa na primeira vez em que uma tela precisa delas, por padrão do servidor de fontes do Google. O build de produção as serve ele mesmo: depois do flutter build web, o tool/build_web.sh roda dart run tool/site_fonts.dart, que lê os endereços dessas fontes compilados no main.dart.js, baixa cada arquivo uma vez para build/web_fonts_cache/ (os builds seguintes reaproveitam; --offline usa só o cache), copia-os para fonts/<família>/v<versão>/ no build, com a licença das fontes, e preenche o fontFallbackBaseUrl no flutter_bootstrap.js; assim nem as páginas nem o app pedem nada a um serviço de fontes (a Política de Segurança de Conteúdo não tem mais servidor de fontes). Com o Flutter 3.47.9 são 725 arquivos, 21,8 MB no servidor: 6,6 MB das escritas dos 20 idiomas da interface, dos símbolos e da matemática (as de chinês e japonês divididas em 101 e 124 fatias), 7,0 MB das variantes de chinês e coreano que um navegador em chinês tradicional, de Hong Kong ou em coreano pede, 2,0 MB de emoji e 6,2 MB de 130 outras escritas. O visitante baixa só as fatias de que o texto na tela precisa, e o service worker as guarda depois de usadas. No flutter run e num flutter build web simples o marcador fica, e o motor usa o servidor do Google como antes.

App instalado. Cada página de entrada tem um manifest no seu idioma (manifest.json, <código>/manifest.json): o mesmo app qualquer que seja a página de onde foi instalado, aberto na página daquele idioma, com os seus atalhos. Os manifests também fazem o app instalado se comportar como um programa do computador:

  • Abrir com: projetos (.oci.json, e um .json cujo formato é o do projeto) e os desenhos, mapas e levantamentos que o app importa (DXF, DWG, DGN, KML, KMZ, GeoJSON, GPX, LAS/LAZ) podem ser abertos com o OpenCADIrrigation pelo gerenciador de arquivos (navegadores Chromium no computador). Um projeto abre numa aba própria (a aba em branco de uma abertura nova é usada), o resto é importado numa aba nova.
  • Uma janela só: uma segunda abertura (um atalho, outro arquivo) vai para a janela já aberta, que vem para a frente e faz o que a abertura pede; nada é recarregado e nenhum trabalho se perde.
  • Compartilhar: no Android, arquivos, fotos e links compartilhados com o app instalado (o alvo de compartilhamento do manifest) chegam ao service worker como um envio para <página>share-target; ele os guarda no cache oci-share e abre a página em ?share=<id>, onde o flutter_bootstrap.js os entrega ao app uma vez e os descarta (um compartilhamento que ninguém abriu é descartado depois de um dia). Uma planta em PDF passa pelo seu diálogo de importação, um projeto ou um desenho abre como acima, um link do OpenCADIrrigation num texto compartilhado abre o seu projeto.

Guia do usuário. O gerador também escreve este manual como páginas do site, de docs/manual (inglês, em /manual/) e de docs/manual/pt-BR (português, em /pt/manual/), com o tool/site_manual.dart: uma página por capítulo, com a lista de capítulos, o sumário das seções, o capítulo anterior e o próximo, as âncoras que o GitHub dá aos títulos (os links entre capítulos continuam funcionando) e links para os outros arquivos do repositório no GitHub. Os outros idiomas apontam para o guia em inglês no rodapé da sua página.

O gerador também escreve o que buscadores e assistentes de IA leem além das páginas: web/robots.txt (tudo liberado, com os robôs de busca e de IA nomeados: GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended, CCBot, bingbot e outros), web/llms.txt (o resumo do llmstxt.org: o que é o app, fatos principais e links para a página de cada idioma e cada exemplo) e web/llms-full.txt (a referência completa em Markdown: recursos, fluxo de projeto, métodos hidráulicos, dimensionamento, catálogos, formatos, links diretos, limitações, perguntas frequentes; as seções técnicas ficam em tool/site/llms.dart e seguem o manual em inglês), web/humans.txt e a chave IndexNow web/<chave>.txt. Depois de publicar, dart run tool/indexnow.dart pede ao Bing, ao Yandex e aos outros buscadores do IndexNow que rastreiem as páginas de novo (o índice do Bing também responde ao Copilot e à busca do ChatGPT); --dry-run mostra a requisição sem enviá-la.

Revisão das traduções por falantes nativos

Os testes conferem em cada pacote as chaves que faltam, os marcadores e o glossário, mas só um falante nativo sabe dizer se um texto soa bem. tool/i18n_review.dart transforma um idioma numa planilha que o revisor preenche sem abrir JSON nem código, e traz o arquivo revisado de volta para o pacote:

dart run tool/i18n_review.dart export ha                  # build/i18n_review/i18n-review-ha.csv
dart run tool/i18n_review.dart export all --format xlsx   # todos os idiomas menos o inglês, em XLSX
dart run tool/i18n_review.dart import i18n-review-ha.xlsx --dry-run   # o que mudaria, sem gravar nada
dart run tool/i18n_review.dart import i18n-review-ha.xlsx             # grava assets/i18n/ha.json

O arquivo. Uma linha por texto do idioma: a tabela de textos, o tutorial e os nomes do catálogo padrão (português e espanhol vêm das tabelas compiladas; o inglês é a origem e não é exportado). As colunas são status, section (strings, tutorial, catalog), key (a chave do texto; para o tutorial e o catálogo, o próprio texto em inglês), english, current (<código>) (a tradução que o idioma tem hoje), review e comment (vazias, para o revisor), notes, context (onde o texto aparece: os arquivos da interface que mostram a chave, o tutorial, o catálogo) e order (o lugar do texto no programa: ordenar por ela devolve cada texto para junto dos vizinhos). As linhas vêm das mais urgentes para as menos urgentes, pelo status: missing (ainda sem tradução: o app mostra inglês), fix (a tradução quebra uma regra: um marcador perdido, uma palavra que o glossário evita, um nome que deve ficar como em inglês), provisional (o texto em inglês mudou e o pacote espera uma tradução nova), same-as-english (deixado em inglês onde se espera uma tradução), translated, may-stay-english (uma sigla, um nome de método ou de arquivo que o glossário permite em inglês) e keep-english (nomes, símbolos e fórmulas, iguais em todos os idiomas). As notas dizem o que tool/i18n/glossary.json pede daquele texto: por que ele pode ou deve ficar em inglês, o termo a usar para um conceito que o texto em inglês cita e as palavras a evitar (Glossary: “emitter” is “Auslass”, never “Emitter”…), os marcadores a manter ({n}, {name}…) e, para um texto do tutorial ou do catálogo cujo inglês mudou, a tradução anterior como ponto de partida. O CSV segue a RFC 4180, em UTF-8 com a marca de ordem de bytes (BOM), para que os programas de planilha leiam todas as escritas. O XLSX tem as mesmas colunas na primeira aba e as instruções na segunda; é a escolha mais segura, porque um programa de planilha pode tomar por fórmula uma célula de CSV que começa com + ou -.

A revisão. O revisor escreve um texto em review só onde a tradução atual falta, está errada ou pode melhorar, deixa as outras linhas vazias, pode ordenar, filtrar e mover as colunas, põe em comment qualquer pergunta para os mantenedores e devolve o arquivo salvo como XLSX ou como CSV em UTF-8. O separador ponto e vírgula ou tabulação, como alguns programas de planilha gravam, e o "texto Unicode" UTF-16 deles também são lidos; qualquer outra codificação é recusada em vez de adivinhada.

A importação. import acha cada linha pela seção e pela chave, e tira o idioma do cabeçalho current (<código>), de um nome de arquivo terminado em -<código>.csv ou -<código>.xlsx, ou de --lang. Ela recusa a revisão cujos marcadores não são os do texto em inglês, a que muda um texto que o glossário mantém em inglês (untranslatable), a que usa uma palavra que os terms do glossário evitam naquele idioma, uma seção ou uma chave que o programa não tem (a chave desconhecida é informada, nunca acrescentada), uma linha revisada duas vezes com textos diferentes e, num pacote, um texto deixado em inglês onde o test/core/i18n_glossary_test.dart espera uma tradução (numa escrita não latina; num pacote de escrita latina, além do seu maxSameAsEnglish). Ela mostra o que muda, o que fica e o que é recusado, com o motivo, avisa quando o texto em inglês ou a tradução mudaram desde a exportação, lista os comentários do revisor e não grava nada quando alguma revisão é recusada, a não ser com --force-partial (o código de saída continua 1); --dry-run não grava nada. O pacote é editado no lugar: um texto alterado é trocado onde está, uma chave que falta é inserida onde a tabela de textos a tem, com a indentação das vizinhas, e todo o resto do arquivo fica byte a byte como estava, então o diff de uma revisão mostra só as linhas revisadas. Português e espanhol vêm compilados no app (lib/app/i18n.dart e suas partes, lib/app/tutorial/tutorial.dart, lib/core/model/catalog.dart): as mudanças deles são listadas, cada uma com o arquivo, o texto antigo e o novo e a entrada para colar, e salvas ao lado do arquivo revisado como <nome>.<código>-changes.txt, para aplicar à mão. Depois de importar, rode flutter test test/core/i18n_packs_test.dart test/core/i18n_glossary_test.dart; a ferramenta em si é testada por test/i18n_review_test.dart.