dialect 0.1.0-dev
dialect: ^0.1.0-dev copied to clipboard
AI-native localization toolkit for Flutter-led teams.
Changelog #
All notable changes to the Dialect CLI are tracked here. Pre-1.0 entries are
work-in-progress milestones from planning/mvp-plan.md.
[Unreleased] #
0.1.0-dev #
The pre-1.0 development line. Every milestone from M0 (convention
validation) through M11 (distribution pipeline) is rolled into
0.1.0-dev until the v1.0.0 tag promotes the spec contracts to
stable. Detailed per-milestone notes follow.
Added #
-
M11. Distribution pipeline — four channels (Docker + Scoop dropped per scope decision).
install.shat the repo root. POSIXsh, OS/arch detection (macos-arm64,macos-x64,linux-x64,linux-arm64), version pinning viaDIALECT_VERSION, install location viaDIALECT_INSTALL_DIR(default~/.local/bin), SHA-256 checksum verification against the release'sSHA256SUMSasset. Refuses Windows with a pointer to the GitHub release zip. Hosted athttps://dialect.tools/install.sh; also published as a release asset so the GitHub URL is a stable fallback..github/workflows/release.yml— tag-driven (v*) matrix build acrossmacos-latest(arm64),macos-13(Intel x64),ubuntu-latest(x64),ubuntu-24.04-arm(arm64), andwindows-latest. Each runner: pnpm install + build the dashboard,tool/build_dashboard.dart --no-pnpmto bake the SPA,dart compile exe, tarball/zip + SHA-256. Aggregate job composesSHA256SUMS, attaches every artifact +install.shto the GitHub release. Separatepubjob publishes to pub.dev via OIDC (no secret token). Separatehomebrewjob renders the formula fromhomebrew/dialect.rb.tmpland opens a bump-PR againstChauCM/homebrew-tap(skipped for prereleases —v1.0.0-rc.1etc. don't ship to Homebrew).action.ymlat the repo root — composite GitHub Action.uses: ChauCM/dialect@v1withargs:(defaultcheck --strict),version:(defaultlatest), andworking-directory:inputs. Installs via the sameinstall.shscript so the action and the curl-installer share one code path.homebrew/dialect.rb.tmpl— formula template; per-target{{*_SHA256}}and{{VERSION}}placeholders. Branched byHardware::CPU.arm?so a single formula covers macOS arm64, macOS x64, linux arm64, linux x64.tool/render_homebrew_formula.dart— reads the template, fetchesSHA256SUMSfrom the GitHub release for a tag, fills in every placeholder, aborts non-zero if any per-target hash is missing (so the bump-PR can't ship a half-rendered formula).LICENSE.md→LICENSEper pub.dev convention.CHANGELOG.mdgains a top-level0.1.0-devheading per pub.dev convention so the nextdart pub publish --dry-runis down from 4 warnings to the single deferreddocs/ → doc/rename note.- Dropped from the original 6-channel plan: Docker (no expected adoption signal yet; can re-add post-v1.0 if users ask) and Scoop (Windows package manager; Windows users get binaries via the GitHub release zip until there's signal).
-
M10. Svelte dashboard SPA +
dialect serve.- Dart Shelf server at
lib/server/server.dartbindslocalhost:4077by default (configurable via--port/--host). Each request reloads the project from disk so external ARB edits are visible immediately — the project is tiny, I/O is cheap, and it removes the stale-cache foot-gun. - REST API per
docs/architecture.md§ REST API:GET /api/config— parseddialect.yamlplus the resolved project name.GET /api/strings?locale=<loc>— every source key with the matching translation entry. Includesdescription,context,placeholders,locked,glossary_exempt,source_hash, and computedstale+missingflags.PUT /api/strings/<key>— body{ locale, value, locked?, glossary_exempt? }. Writes back throughArbWriter(canonical formatting). Locking writes@key.source_hashperdialect/spec/source_hash.md; unlocking clears it. Empty metadata blocks are dropped (no@key: {}noise on translation files).GET /api/glossary— parsedglossary.yaml.GET /api/status— reuses the M6computeStatusmath byte-for-byte, so the dashboard footer and the CLI agree.OPTIONSpreflight + permissive CORS so the SPA can run underpnpm devagainst the Dart server during dashboard development.- JSON 404 for unknown
/api/*paths (never falls through to the SPA HTML handler); JSON 500 for uncaught exceptions.
- Svelte 5 SPA (
dashboard/, built with Vite, package manager pinned to pnpm):App.svelte— locale switcher + filter sidebar + translation table + coverage footer.lib/TranslationTable.svelte— keyboard-driven inline editing (Enter to save, Esc to cancel, blur saves, optimistic refresh of strings + status after each PUT).lib/GlossaryHighlight.svelte— whole-word glossary highlighting of the source string with the canonical-translation tooltip.lib/LockToggle.svelte— pin/lock control wired through thelockedfield on PUT.lib/FilterPanel.svelte— missing/locked/stale toggles, namespace radio group, search box.lib/CoverageFooter.svelte— coverage % + missing/stale/locked counts from/api/status.lib/api.ts— typedfetchConfig/fetchStrings/fetchGlossary/fetchStatus/putStringwrappers.
- Static-asset embedding.
tool/build_dashboard.dartwalksdashboard/dist/afterpnpm buildand emitslib/server/embedded_assets.g.dart— a singleconst Map<String, List<int>>from forward-slash paths to file bytes.dart compile exe bin/dialect.dart -o build/dialectproduces an 8.1 MB self-contained binary that serves the SPA with zeronode_moduleson disk at runtime. Generator supports--no-pnpm(CI pre-builds) and--check(drift gate). - Empty-bundle fallback. When the generator hasn't run on
this checkout,
/serves a minimal "Dashboard not bundled yet" HTML page that names the exact command to bake it in. The REST API stays live so backend work can proceed without the SPA build. - Real implementation of
dialect serve(was a stub).--port/--hostflags, project-load preflight surfaces "no project" as exit 66 before binding the port, SIGINT clean shutdown. ArbMetadata.copyWith— new helper with a sentinel forsourceHashso callers can explicitly clear the field (unlock) without colliding with the "leave it alone" default. Powers the PUT mutation path.- 13 new server route tests (now 178 total). Real E2E verified
against
build/dialect serve example/: PUT writesReservar AHORA+@key.source_hashtoes.arbon disk; unlock reverts both.
- Dart Shelf server at
Added #
- M9. Stable on-disk-contract spec docs under
dialect/spec/.icu-json.md— backend JSON output that preserves ICU plural/select expressions byte-identically. Flat keys (not nested objects), one file per locale, UTF-8 NFC, 2-space LF, sorted. Aligned withDialect.AspNetCore(v1.1) and the third-party backend libraries documented indocs/platforms-backend.md.flat-json.md— sibling format that strips ICU plural/select/selectordinal to a single plain string by taking theotherbranch and recursively stripping nested expressions. Documents the loss-of-information trade-off and theinfohintdialect syncemits when a project's source uses plurals andflat-jsonis selected.state.md—.dialect/state.jsonshape for soft-modedialect checkacknowledgements (decision 9 in the plan). Per-issue acks keyed by<rule>:<locale>:<key>, fingerprinted with the source/translation hash at ack-time so edits resurface the warning. Structural rules are explicitly NOT ack-able (those are correctness failures, not heuristics).- All three carry the same status header as
source_hash.md: v1.0 stable contract, breaking changes require a major bump. - README.md gains a "Stable on-disk contracts" sub-section linking every spec — backend integrators land on the contract before they write a single line of glue code.
- Blocks the v1.0.0 tag per the plan. The
Dialect.AspNetCoreNuGet (v1.1) depends on a stableicu-jsoncontract; shipping v1.0 without versioned specs would cost us users at the first breaking change.
Added #
- M8.
dialect checksemantic heuristics — four new rules layered on the M4Ruleinterface, runner, and report.source_equality(warning) — flags translations identical to the source. Filters short / symbol-only values; honors@key.locked: trueas the explicit dismissal.length_ratio(warning) — flags translations whose char count is outside[min, max] * source.length. Default band[0.3, 2.5]. Per-locale overrides vialength_ratio:indialect.yaml. Source values shorter than 8 chars skip the check (too noisy on "OK"/"Hi"-class strings). Reports the actual ratio in the message.untranslated_english(warning) — flags translation values containing a conservative whole-word English function word (the/and/with/this/that/you) that isn't carried over from the source. Bias: under-flag rather than wrongly flag.glossary(warning) — for every source value containing a glossaryterm:(whole-word, case-insensitive), each target translation must contain a recognizable prefix of the canonicaltranslations.<locale>. Suffix-inflection tolerance baked in (drops last 2 chars when len > 4, so "Reservar" matches "Reserva"/"Reservamos"). Honors@key.glossary_exempt: trueon the source entry — the documented escape hatch for non-literal uses.
lib/glossary/glossary_loader.dart— typed loader fordialect/glossary.yaml. Empty when file absent (no error); throwsFormatExceptionon malformed YAML. Loaded once atDialectProject.loadand exposed asproject.glossaryso every rule reads from the same in-memory snapshot.--strict-lengthplumbing.report.dartlearned to honor the flag independently of--strict: under bare--strict, every warning promotes to error exceptlength_ratio(length ratios are noisy enough that a blanket CI gate produces too many false positives).--strict-lengthis the explicit opt-in.- CHANGELOG note for example/ polish. Running
dialect checkon the canonicalexample/now surfaces 7 real semantic warnings (source-equality onTotal/Emailcarryovers; glossary misses in ar; length-ratio edge cases for ja/ar). Soft mode exits 0, so the integration test stays green — but these are real signals the demo polish should address (lock the legit carryovers, widen the per-locale length bands).
Added #
- M7.
dialect import+dialect describe— AI-pointer flow.dialect import --from <fmt> --path <path>writes.dialect/import-plan.md.dialect describe [--path <path>]writes.dialect/describe-plan.md. Dialect itself never opens.dart/.kt/.swift/.csfiles; the plan tells the user's AI where to look. CLAUDE.md §3.1 made literal.- Plan-file content is the load-bearing product surface. Each plan
is self-contained — it inlines the load-bearing convention rules
(key style, namespaces, "what NOT to extract", placeholder
preservation, ICU plural shape, glossary application, hard
guardrails) so a stock AI agent doesn't need follow-up prompting,
and points at
dialect/dialect.yamlas the canonical source if anything conflicts. - Templates live at
templates/import_plan.mdandtemplates/describe_plan.md(reviewable in PRs as Markdown);tool/sync_templates.dartbakes them intolib/templates/{import,describe}_plan_md.dart. Runtime token substitution vialib/templates/plan_render.dart. Tokens:{{FROM}},{{PATH}},{{SOURCE_LOCALE}},{{TARGET_LOCALES}},{{PROJECT_NAME}}(readsproject.namefromdialect.yamlwith a friendly fallback),{{NAMESPACES}}(sorted union across platforms),{{GENERATED_AT}}(ISO 8601 UTC). Unknown tokens are left in place so seed-file typos are loud, not silent. - Plan files overwrite on every run —
.dialect/is gitignored ephemera per M3, not durable state. - Exit codes consistent with
status/check: 0 success / 64 usage (missing--pathon import, unknown--from) / 65 malformeddialect.yaml/ 66 no project.
Added #
- M6.
dialect status— per-locale coverage table.- Columns:
Locale,Coverage,Stale,New,Locked. Output via a Unicode box-drawing table (lib/render/table.dart). Coverage=(translated keys) / (source keys).New= source keys missing from the translation.Locked= entries with@key.locked: true.Stale= locked entries whose stored@key.source_hashno longer matches the current source value's hash.
- Columns:
@key.source_hashspec atdialect/spec/source_hash.md. SHA-256 of the NFC-normalized source value, truncated to 16 lowercase hex chars. Written by the dashboard at lock-time (M10); read by status, the dashboard's stale indicator, anddialect translate --skip-locked(M8+). Hashing the value only — not description or placeholders — so a description edit doesn't invalidate locked translations.lib/arb/source_hash.dart—computeSourceHashimplementing the spec. 6 tests including a locked-in fixture so future implementations can't silently change the on-disk fingerprint format.lib/render/table.dart— generic Unicode-table renderer. Right-aligns numeric data columns, left-aligns text and the header. Zero deps. 5 tests.
Added #
- M5.
dialect sync— ARB-passthrough adapter.- Walks
platforms:indialect.yaml. For each platform withformat: arb, writes<output>/app_<locale>.arbfiles (Flutter'sgen_l10nconvention). - Source ARB output keeps
@keymetadata; translation outputs strip metadata per the convention. - Namespace filter: keys whose prefix (before the first
.) isn't inplatforms.<p>.namespacesare dropped from that platform's output. Empty namespaces list = no filtering. - Output paths resolved against the project root (not cwd), so
dialect sync /path/to/projandcd /path/to/proj && dialect syncwrite identical bytes to the same locations. - Idempotent: re-running
syncwith no changes does not touch any files (mtime preserved)._maybeWriteskips writes when the desired bytes already match disk. - Sync does not auto-fix the input ARBs. Run
dialect check --fixseparately to normalize sources; sync produces canonical output regardless (because it goes throughArbWriter). - Non-
arbformats print a "lands in v1.1" hint and skip without erroring.
- Walks
DialectConfig.platformsparsing — typedPlatformConfig(name,output,format,namespaces).length_ratioandprojectblocks still flow throughextrasfor M8.lib/adapters/arb_adapter.dart—ArbAdapter.prepare(filter + strip),ArbAdapter.encode(delegates toArbWriter),ArbAdapter.filenameFor(theapp_<locale>.arbconvention; v1.1 spec will make this configurable viaplatforms.<p>.filename_pattern).
Added #
- M4.
dialect check— structural validation +--fixnormalization.- Five structural rules under
lib/checks/structural/:missing_keys,placeholder_match,plural_categories,empty_values,orphan_metadata. Each rule emits typedIssueobjects with severity, locale, key, file path, line number, and a real soft-mode hint (not a stub). TheRuleinterface is shared with M8 semantic rules. - CLDR table (
lib/checks/cldr_categories.dart) covers ~25 common locales; unknown locales emit a friendly warning rather than silently skipping. - Position tracking added to the parser: every top-level key gets a
1-based line number (
ArbFile.entryLines) via a regex post-pass over the source text. Powersfile:linehints in the check report. Both"key":and"@key":lines are recorded so orphan metadata gets a position too. - Project loader (
lib/project/dialect_project.dart) readsdialect.yaml+ source ARB + every target translation. MinimalDialectConfigparser; M5 extends withplatformsandlength_ratio. --fixmode re-emits every ARB throughArbWriter: sorts keys, hoists@@locale, places@keyblocks correctly, strips translation metadata, drops orphans. Re-runs the check pass on the rewritten files so the exit code reflects post-fix state.- Real-world fixture coverage: the plural-categories rule uses
_validation/runs/{gpt-5-3,claude-post-patch}/dialect/translations/ar.arbas the negative / positive fixtures (the M0+ Codex defect → live bug fixture, no synthetic mockery). - Exit codes: 0 clean / 1 errors / 64 usage / 65 malformed ARB or YAML
/ 66 no project. Soft mode exits 0 for warnings-only;
--strictpromotes everything to errors.
- Five structural rules under
Changed #
- M3 follow-up. Three small fixes before M4 starts.
templates/glossary.yamlnow ships with one example term + a "Replace or delete this entry" hint, the same pedagogical role ascommon.exampleinsource/en.arb. Was effectively empty before.- SDK floor bumped from
^3.4.0to^3.11.0(current stable Dart). Pre-1.0 policy: track current stable. Locks at "current + previous two minor SDK versions" at 1.0 launch. Policy documented inplanning/mvp-plan.md. tool/sync_templates.dartsimplified — thedart formatpipe workaround removed in favour of// dart format offdirectives in the generated source. The directive landed in Dart 3.7 and is now honored on every supported SDK.
- v1.0.1 backlog (flagged from M3 review, intentionally not blocking
M4):
dialect init --forceis whole-tree overwrite for the canonical files. A separate--refresh(write only missing files) is friendlier for users with customizeddialect.yaml.project.namecould auto-detect from a siblingpubspec.yaml'sname:field, falling back to the directory basename. Currently everyone gets the placeholder "Your project".
Added #
- M3.
dialect init— the first user-facing command.- Accepts an optional positional
[path]argument (defaults to cwd). --forceoverwrites an existingdialect/directory.- Writes
dialect/dialect.yaml,dialect/glossary.yaml,dialect/source/en.arb; createsdialect/translations/. - Appends
.dialect/to.gitignore(creates the file if missing, deduplicates if already present). - Exit codes: 0 success, 64 usage, 65 dialect/ exists w/o
--force, 66 target directory missing.
- Accepts an optional positional
- Templates pipeline. Canonical seed YAML/ARB at repo-root
templates/;tool/sync_templates.dartbakes them into Dart constants underlib/templates/*.dart.--checkmode wired into CI so a hand-edited generated file fails the build. The generator pipes output through in-projectdart formatso the on-disk shape always matches whatdart formatwould produce. 9 init tests intest/commands/init_test.dart(creation, byte-identical templates, .gitignore handling, --force, error cases).
Changed #
- M2 hardening. Pre-emptive fixes to the ARB substrate before M3 builds
on it.
- Parser preserves unknown
@@<name>file-level metadata inArbFile.fileMetadata(e.g. Fluttergen_l10n's@@last_modified). Writer emits it after@@localein sorted order. Prevents silent data loss ondialect sync. - Orphan
@keyblocks (no matching key/value) preserved inArbFile.orphanMetadataso M4 can surface them as structural errors without re-reading raw JSON. Writer skips orphans by construction —dialect check --fixstrips them implicitly. example/dialect/source/en.arbsynced with the canonical 30-key extract from_validation/runs/claude-post-patch/. Matches the translation files now.arb_roundtrip_test.dartresolves the seed viatest/_support/repo_root.dartinstead of acwd-relative path.- Removed unused
intl: ^0.20.2dependency.
- Parser preserves unknown
- Tooling.
tool/sync_version.dartkeepslib/version.dartin lock- step withpubspec.yaml'sversion:field.--checkmode runs in CI so a forgotten sync fails the build. Stricter hand-picked lints (unawaited_futures,avoid_dynamic_calls,prefer_relative_imports,directives_ordering,prefer_final_locals,cancel_subscriptions,close_sinks) layered on top ofpackage:lints/recommended.yaml. - CI.
.github/workflows/ci.ymlrunsdart pub get, version-sync--check,dart format --set-exit-if-changed,dart analyze --fatal-infos --fatal-warnings, anddart teston every push and PR. - Docs.
planning/mvp-plan.mdupdated to reflect thatintl_translationis deprecated and Dialect ships a focused ICU scanner; future agents reading the plan won't try to "fix" the code by ripping out the scanner.
Added #
- M2. ARB read/write + ICU detection (
lib/arb/).arb_file.dart: typed model withdescription,context,placeholders,locked,glossary_exempt,source_hash, plus extras pass-through.arb_parser.dart: reads ARB JSON; NFC-normalizes all string values (unorm_dart) so Vietnamese NFD vs NFC inputs hash identically.arb_writer.dart: emits canonical ARB JSON matching the seed file byte-for-byte (sorted keys,@@localefirst,@keyafter its key, blank-line separators).icu_message.dart: focused scanner exposingextractPlaceholders,extractPluralCategories,hasExpressions. Handles''and'…'ICU escape, nested plural/select branches, selectordinal, and the Round-2 "=N mirrors AND CLDR categories" pattern. 40 tests including byte-identical round-trip on the seed.
- M1. Dart CLI scaffold:
dart create --template=clibaseline,pubspec.yamlwith SDK constraint^3.4.0,args ^2.7.0andlints ^6.0.0dev. Entry point atbin/dialect.dart.DialectCommandRunnerwires the seven v1.0 commands (init,import,describe,sync,check,status,serve) as stubs.--versionand--helpwork. Anti-goal PR checklist in.github/PULL_REQUEST_TEMPLATE.md. - M0+. Multi-model convention convergence test across five models (Claude
Opus, Claude Sonnet 4.6, Composer 2.5, Gemini 3.5 Flash, Codex 5.3). 9 of 10
axes converged. One Round 2 patch (Arabic CLDR plural worked example).
See
example/_validation/COMPARISON.md. - M0. Canonical convention text in
example/dialect/dialect.yaml,glossary.yaml, and seedsource/en.arb. Sample booking-style Flutter app underexample/lib/. Validation report inplanning/convention-validation-report.md.