OpenCADIrrigation Open the app

Languages and site

The interface is in 20 languages, the most spoken in the world. Portuguese, English and Spanish are compiled into the app; Chinese (zh), Hindi (hi), Arabic (ar), French (fr), Bengali (bn), Russian (ru), Urdu (ur), Indonesian (id), German (de), Japanese (ja), Marathi (mr), Vietnamese (vi), Telugu (te), Hausa (ha), Turkish (tr), Punjabi (pa, Gurmukhi script) and Swahili (sw) are language packs in assets/i18n/<code>.json, fetched only when that language is chosen (the interface shows English until the pack arrives, a fraction of a second). Each pack translates the whole string table, the interactive tutorial and the default catalogue names; the design worker receives the pack with the request, so reports and plots come out in the chosen language too. The app opens in the first language of the system's preferred-language list (from the browser or the operating system) that it speaks, and in English when none of them is spoken; the choice in View → Language (each language under its own name) is saved. The language pages (/pt/, /de/, …) and the ?lang= parameter apply to the visit only. The document lang attribute follows the app language (pt-BR, zh-CN, otherwise the code). In Arabic and Urdu the interface is laid out right to left. PDFs print every one of these scripts (Reports, plotting and export): before a PDF is written, the bundled Noto fonts its texts need are loaded once per session. The shipped example projects exist in all 20 languages (examples/<code>/).

A pack is a plain JSON file with three sections: strings (key → text), tutorial and catalog (English source text → text). Translators can revise it without touching the code, and native speakers review it in a spreadsheet (below); test/core/i18n_packs_test.dart checks that every pack covers every key with the same placeholders.

The site is generated by dart run tool/gen_site.dart from tool/site.dart: web/index.html (English, the page a visitor gets when nothing chose a language), one web/<code>/index.html for each of the other 19 interface languages (web/pt/, web/es/, web/de/, web/zh/, …; the list follows supportedLanguages, so a new app language needs its page), a redirect at web/en/index.html for the address English used to have, web/sitemap.xml, web/404.html (in the browser's language when the site speaks it), the privacy policy and the terms of use of every language (web/privacy/, web/<code>/privacy/, and terms/ alike; written in Portuguese, English and Spanish, in English in the others until they are translated, which are then not indexed), one manifest per language (web/manifest.json, web/<code>/manifest.json: the same app identity, a start address, names and shortcuts in that language, a wide and a narrow screenshot), web/site.js (the only script of the pages: it starts the app when it is wanted and asks the consent of the statistics) and the hosting configuration hosting/firebase.json (see hosting/README.md). The page texts live in tool/site.dart (Portuguese, English, Spanish) and tool/site/<code>.dart (the others, with the example gallery taken from the language packs); Arabic and Urdu pages read right to left. Since Flutter paints on a canvas, every page carries the content as real HTML (heading, features, examples, FAQ), with its own canonical, hreflang between all the versions (also the bare pt and zh beside pt-BR and zh-CN), a Content Security Policy (scripts from the site only, WebAssembly for CanvasKit, no inline script, no plugin or frame), Open Graph (with a sharing image in the page's own script, web/og/og-image-<code>.jpg, rendered from the page by node tool/og/render_og.js; English keeps web/og-image.png) and JSON-LD (application, FAQ, how it works and the example gallery, in the page's language, with the subject linked to Wikipedia/Wikidata). The test test/web_metadata_test.dart fails when the generated files drift from the template.

That HTML is all a first visit downloads (and what crawlers and PageSpeed measure): the app, several megabytes, is fetched only when the visitor opens it (the page's buttons and example links start it in place), arrives at an app address (?example=, ?tutorial=, ?design=, ?ai=, ?lang=, the installed app's ?source=pwa) or has used it in that browser before (any opencadirrigation. key in localStorage); the page then stays, hidden, under the app. web/flutter_bootstrap.js is Flutter's loader without Flutter's deprecated service worker, and tool/build_web.sh drops the pointer to the flutter.js.map the build does not write and builds with --no-web-resources-cdn: CanvasKit and the Roboto font come from the site itself, not from Google's CDN. The logo at the top is WebP in three widths (web/brand/logo-horizontal-{520,780,1040}.webp, made from logo-horizontal.png), its address versioned by a hash of the file. The site is served by Firebase Hosting from a deploy folder kept outside this repository (public/, a copy of build/web, beside firebase.json); the configuration is versioned in hosting/firebase.json, generated with the pages (hosting/README.md says how to apply it). It has no catch-all rewrite, so an unknown address gets 404.html with status 404 instead of the app page (which made /.well-known/ai-catalog.json look like a broken AI catalogue to PageSpeed); every file gets nosniff, SAMEORIGIN, a Referrer-Policy and a Permissions-Policy limited to the device position and the microphone; the pages, the manifests and every app file whose name stays the same from build to build (main.dart.js, the worker, flutter_bootstrap.js, sw.js, offline.js, site.js, assets/**, canvaskit/**) are no-cache, the images under brand/, icons/, screenshots/ and at the root keep for 30 days, and /en/ answers with a 301 to /.

Offline start. Once the first frame is on screen and the browser is idle, flutter_bootstrap.js loads web/offline.js, which registers the app's own service worker, web/sw.js, so a first visit is never slowed by it. tool/build_web.sh ends with dart run tool/gen_sw.dart, which writes into the build's sw.js the build name (version, git commit, date and a hash of the files) and the files a start may need with their SHA-256: the root page, the bootstrap, main.dart.js, the design worker, site.js, the manifest and icons, the Flutter assets, the logo and CanvasKit (the browser's variant only), about 17 MB uncompressed. The language packs, the PDF fonts and the licence list are listed with their hash too but kept only once a start uses them, and a new build carries forward those the browser kept, fetching their new version when they changed. At install the worker fills one cache per build (oci-shell-<build>): files unchanged since the previous build are copied from its cache, the others fetched past the HTTP cache and checked against their hash. It answers from the cache first and revalidates the start page after the first frame; it keeps the fallback fonts the engine fetches from fonts/ once used (their addresses carry their version, so one copy serves every build); map tiles, elevation, climate and analytics go to the network untouched. A new build is installed in the background; offline.js tells the app, which offers to reload into it (A new version of OpenCADIrrigation is ready, after a word about unsaved work, which goes to the autosave), and otherwise it is taken at the next start. A page already open keeps the build it started with (the other open pages keep theirs when one reloads), a page left open for days looks for a new build every hour and when it comes back on screen, and a build's cache is deleted once no page uses it. A sw.js that was not filled in (flutter run, a plain flutter build web) only removes itself and its caches.

Fonts served by the site. Flutter bundles Roboto (Latin, Cyrillic, Greek); the glyphs of every other script and the symbols come from the fallback fonts of the engine, which it fetches the first time a screen needs them, by default from Google's font server. The production build serves them itself: after flutter build web, tool/build_web.sh runs dart run tool/site_fonts.dart, which reads the addresses of those fonts compiled into main.dart.js, downloads each file once into build/web_fonts_cache/ (later builds reuse it; --offline uses only the cache), copies them to fonts/<family>/v<version>/ in the build with the font licence, and fills in fontFallbackBaseUrl in flutter_bootstrap.js, so neither the pages nor the app ever ask a font service for anything (the Content Security Policy has no font host left). With Flutter 3.47.9 that is 725 files, 21.8 MB on the server: 6.6 MB for the scripts of the 20 interface languages, the symbols and the mathematics (the Chinese and Japanese ones split into 101 and 124 slices), 7.0 MB for the Chinese and Korean variants a browser in Traditional Chinese, Hong Kong or Korean asks for, 2.0 MB of emoji and 6.2 MB for 130 other scripts. A visitor downloads only the slices the text on screen needs, and the service worker keeps them once used. Under flutter run and in a plain flutter build web the placeholder stays and the engine uses Google's server as before.

Installed app. Every entry page has a manifest in its language (manifest.json, <code>/manifest.json): the same app whatever page it is installed from, started on the page of that language, with its shortcuts. The manifests also make the installed app behave like a program of the computer:

  • Open with: projects (.oci.json, and a .json whose format is the project's) and the drawings, maps and surveys the app imports (DXF, DWG, DGN, KML, KMZ, GeoJSON, GPX, LAS/LAZ) can be opened with OpenCADIrrigation from the file manager (Chromium browsers on the computer). A project opens in a tab of its own (the blank tab of a fresh start is used), anything else is imported into a new tab.
  • One window: a second launch (a shortcut, another file) goes to the window already open, which comes to the front and does what the launch asks; nothing is reloaded and no work is lost.
  • Share: on Android, files, pictures and links shared with the installed app (the share target of the manifest) reach the service worker as a post to <page>share-target; it keeps them in the cache oci-share and opens the page at ?share=<id>, where flutter_bootstrap.js hands them to the app once and drops them (a share nobody picked up is dropped after a day). A PDF plan goes through its import dialog, a project or a drawing opens as above, an OpenCADIrrigation link in a shared text opens its project.

User guide. The generator also writes this manual as pages of the site, from docs/manual (English, at /manual/) and docs/manual/pt-BR (Portuguese, at /pt/manual/), with tool/site_manual.dart: one page per chapter with the list of chapters, a table of contents of its sections, the previous and next chapter, the anchors GitHub gives the headings (links between chapters keep working) and links to the other files of the repository on GitHub. The other languages link to the English guide from the foot of their page.

The generator also writes what search engines and AI assistants read besides the pages: web/robots.txt (everything allowed, with the search and AI crawlers named: GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot, Google-Extended, Applebot-Extended, CCBot, bingbot and others), web/llms.txt (the llmstxt.org summary: what the app is, key facts and links to every language page and example) and web/llms-full.txt (the whole reference in Markdown: features, workflow, hydraulic methods, sizing, catalogues, formats, deep links, limitations, FAQ; its technical sections live in tool/site/llms.dart and follow this manual), web/humans.txt and the IndexNow key web/<key>.txt. After a deploy, dart run tool/indexnow.dart asks Bing, Yandex and the other IndexNow engines to crawl the pages again (Bing's index also answers Copilot and ChatGPT search); --dry-run prints the request without sending it.

Native-speaker review of the translations

The tests check every pack for missing keys, placeholders and the glossary, but only a native speaker can say whether a text reads well. tool/i18n_review.dart turns a language into a spreadsheet that a reviewer fills in without opening JSON or code, and brings the reviewed file back into the pack:

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   # every language but English, as XLSX
dart run tool/i18n_review.dart import i18n-review-ha.xlsx --dry-run   # what would change, nothing written
dart run tool/i18n_review.dart import i18n-review-ha.xlsx             # writes assets/i18n/ha.json

The file. One row per text of the language: the string table, the tutorial and the default catalogue names (Portuguese and Spanish come from the compiled tables; English is the source and is not exported). The columns are status, section (strings, tutorial, catalog), key (the string key; the English text itself for the tutorial and the catalogue), english, current (<code>) (the translation the language has now), review and comment (empty, for the reviewer), notes, context (where the text appears: the interface files that show a string, the tutorial, the catalogue) and order (the place of the text in the program: sorting by it puts every text back beside its neighbours). The rows come most urgent first, by status: missing (no translation yet: the app shows English), fix (the translation breaks a rule: a placeholder lost, a word the glossary avoids, a name that must stay as in English), provisional (the English text changed and the pack waits for a new translation), same-as-english (left in English where a translation is expected), translated, may-stay-english (an acronym, a method or a file name the glossary allows in English) and keep-english (names, symbols and formulas, the same in every language). The notes say what tool/i18n/glossary.json asks of the text: why it may or must stay in English, the term to use for a concept the English text names and the words to avoid (Glossary: “emitter” is “Auslass”, never “Emitter”…), the placeholders to keep ({n}, {name}…) and, for a tutorial or catalogue text whose English changed, the previous translation as a starting point. The CSV follows RFC 4180, in UTF-8 with a byte order mark so that spreadsheet programs read every script. The XLSX has the same columns on its first sheet and the instructions on the second; it is the safer choice, because a spreadsheet program may take a CSV cell that starts with + or - for a formula.

The review. The reviewer writes a text in review only where the current translation is missing, wrong or can be better, leaves the other rows empty, may sort, filter and move the columns, puts any question for the maintainers in comment, and sends the file back saved as XLSX or as CSV in UTF-8. A semicolon or tab separator, as some spreadsheet programs write, and their UTF-16 "Unicode text" are read too; any other encoding is refused rather than guessed.

The import. import finds each row by section and key, and takes the language from the current (<code>) header, from a file name ending in -<code>.csv or -<code>.xlsx, or from --lang. It refuses a review whose placeholders are not those of the English text, one that changes a text the glossary keeps in English (untranslatable), one that uses a word the glossary's terms avoid in that language, a section or a key the program does not have (an unknown key is reported, never added), a row reviewed twice with different texts and, in a pack, a text left in English where test/core/i18n_glossary_test.dart expects a translation (in a non-Latin script; in a Latin-script pack, beyond its maxSameAsEnglish). It prints what changes, what stays and what is refused with the reason, warns when the English text or the translation changed since the export, lists the reviewer's comments, and writes nothing when a review is refused unless --force-partial is given (the exit status stays 1 then); --dry-run writes nothing at all. The pack is edited in place: a changed text is replaced where it stands, a missing key is inserted where the string table has it, with the indentation of its neighbours, and every other byte of the file stays as it was, so the diff of a review shows only the reviewed lines. Portuguese and Spanish are compiled into the app (lib/app/i18n.dart and its parts, lib/app/tutorial/tutorial.dart, lib/core/model/catalog.dart): their changes are listed instead, each with its file, the old and the new text and the entry to paste, and saved beside the reviewed file as <name>.<code>-changes.txt, to apply by hand. After an import, run flutter test test/core/i18n_packs_test.dart test/core/i18n_glossary_test.dart; the tool itself is tested by test/i18n_review_test.dart.