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.jsonwhose 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 cacheoci-shareand opens the page at?share=<id>, whereflutter_bootstrap.jshands 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.
Open the app