This repository contains an OCaml-based internationalization (i18n) string extraction tool. It parses source files (JS, TS, Vue, Pug, HTML, Astro) and extracts strings for translation management.
-
ARCHITECTURE.md: Contains a deep-dive into the codebase layout, directory structure, and a comprehensive API reference. Read this file first when:
- Starting a new task to understand which files are relevant.
- Investigating the impact of changes across the system.
- Looking for specific functionality or function definitions before searching.
-
DEVELOPMENT.md: Contains instructions for environment setup, build processes for various platforms, and release workflows. Read this file first when:
- Setting up the development environment or installing dependencies (OCaml, JS, QuickJS, Flow).
- Building the project for development or release.
- Executing the tool for manual verification or testing.
- Managing version numbers or release artifacts.
- Language: OCaml (5.1.1 in CI) with some C++ (QuickJS bridge) and JavaScript (parsers via Browserify).
- Architecture:
src/cli/: Main entry point (strings.ml), command-line interface, output generation (.strings/.json), and Vue file splitting (vue.ml).src/parsing/: OCaml parsers usingAngstromfor custom formats andFlow_parserfor JS.src/quickjs/: Bridge to QuickJS to run JavaScript-based parsers (TypeScript/Pug) from OCaml.src/utils/: Common utilities for collection, timing, and I/O.
- Key Libraries:
Core,Lwt(concurrency),Angstrom,Yojson,Ppx_jane. - Active branch context: This codebase is the Lwt variant (an Eio port exists on other branches). CI runs on branches
lwtandtest-suite. Concurrency code usesLwt.Syntax/Lwt_io, andStrings.parsereturnsstring Core.String.Table.t Lwt.t.
- Development build:
dune build src/cli/strings.exe - Watch mode:
dune build src/cli/strings.exe -w - Release build:
DUNE_PROFILE=release dune build src/cli/strings.exe - Full release cycle (strip, Docker/Linux): see
DEVELOPMENT.md. - If
duneis not on PATH, runeval $(opam env)first (or prefix withopam exec --).
eval $(opam env)
dune runtest tests/This runs both the inline unit tests (tests/test_runner.ml) and an integration test defined as a runtest rule in tests/dune, which builds the CLI, runs it against tests/fixtures/ in a temp directory, and verifies that existing French translations are preserved and MISSING TRANSLATION markers are emitted.
- After building:
./_build/default/src/cli/strings.exe [directory-to-extract-from] - The CLI fails with "This program must be run from the root of your project" unless the working directory contains either a
strings/directory or a.gitdirectory. - Output directory defaults to
strings/; override with--output DIR(-o). - All long flags require the full
--form (~full_flag_requiredis set everywhere).
--output DIR/-o: change output directory (defaultstrings).--ts: treat scripts in HTML/Pug element attributes as TypeScript.--slow-pug/--sp: use the official Pug parser via QuickJS instead of the fast native OCaml one.--debug-pug/--dp,--debug-html/--dh, and--debug-astro/--da: debug template parsing (mutually exclusive). The first two target.vuefiles;--debug-astrotargets.astrofiles.- There is no
--show-debuggingflag.
- Flow symlinks:
src/flow_parser,src/sedlex, andsrc/collectionsare symlinks into a clonedflowrepo (v0.183.1) at the project root. If they're missing or dangling, builds fail with module errors. Recreate perDEVELOPMENT.md. - QuickJS dependency: Requires a compiled
quickjsdirectory (quickjs-2021-03-27,makerun) at the project root.dunerules insrc/quickjs/dunecopyquickjs.h,libquickjs.a, and invokequickjs/qjscfrom there. - Generated runtime:
src/quickjs/runtime.his generated at build time fromsrc/quickjs/parsers.jsvianpx browserifythenqjsc. Requiresnpm install --no-save typescript browserify pug-lexer pug-parser pug-walkat the repo root. - libomp:
src/quickjs/dunesearches a fixed list of paths (with globs) forlibomp.a/libgomp.a(Homebrew Cellar globs on macOS,/usr/lib/...on Linux). If your system has it elsewhere, the build fails with "Could not find libomp.a" — add your path to the list insrc/quickjs/dune. - Link flags: Platform/profile-specific link flags live in
src/cli/link_flags.{system}.{dev,release}.dune(the Linux dev one is just()). A missing file for your platform/profile combination breaks the build. - Version number:
let version = "x.y.z"insrc/cli/strings.mlmust be bumped manually for releases (the release workflow warns if the tag doesn't match). - Releases are automated: pushing a
v*tag triggers.github/workflows/release.yml, which buildsstrings.linux.tar.gz(Docker) andstrings.mac(arm64-only,macos-latest), then publishes a GitHub Release with SHA256 checksums and a## Changessection generated byscripts/generate-release-notes.mjs(Z.ai; needsZAI_API_KEYsecret, optionalRELEASE_MODELvar, defaults toglm-5.1). Notes generation failures never fail the release. See "Releases" inDEVELOPMENT.md.
- Direct Parsers: Simple formats like
.strings,HTML, and basicVuetags are parsed usingAngstrominsrc/parsing/. - JS/TS Parsing:
- JavaScript uses
Flow_parserand a custom AST walker insrc/parsing/js_ast.ml. - TypeScript uses the official TS parser running inside QuickJS (
src/quickjs/).
- JavaScript uses
- Pug Parsing: Has a "fast" OCaml implementation (
src/parsing/pug.ml) and a "slow" official Pug implementation via QuickJS (enabled with--slow-pug). - Astro Parsing: Native Angstrom scanner (
src/parsing/astro.ml) segments.astrofiles into frontmatter,<I18n>/<i18n>blocks,{...}expressions, and<script>blocks. All Astro possible-scripts are parsed as TSX regardless of--ts(process_file ~template_script:Vue.TSXinsrc/cli/strings.ml;Quickjs.Typescript_tsx→extractFromTSXinsrc/quickjs/parsers.js), so JSX inside expressions parses.Astro.collectalso re-scans expression segments containing<I18n/<i18nfor nested I18n blocks (conditional/mapped JSX rendering). A missingis:rawon an<I18n>whose text contains{produces a non-fatal warning.
- Content is extracted into a
Utils.Collector.t({ path; strings: string Queue.t; ... }). - The collector tracks found strings, potential scripts (to be further parsed), file errors (fatal,
❌), and warnings (non-fatal,⚠️). UseCollector.blit_transferto merge collectors. - Convention: Strings found inside
L("...")calls are treated as translations in JS/TS. In Vue/HTML/Pug templates and Astro files, text insidei18n/I18nelements is a translation key.
- Uses
Lwtfor cooperative concurrency. - Parallel traversal of directories is handled in
src/cli/strings.mlviaLwt_listandLwt_pool. - JS workers (QuickJS) are managed via
Lwt_poolandLwt_preemptiveinsrc/quickjs/quickjs.ml. - Angstrom parsing of channels uses
Angstrom_lwt_unix.parsetaking anLwt_io.input_channel(not raw strings or Eio flows).
- OCamlFormat:
.ocamlformatdefines a custom "Asemio Style" (margin 106,break-cases=all,if-then-else=keyword-first, etc.). Format OCaml code before submitting. - Memory Safety: Be cautious with C++ FFI code in
src/quickjs/quickjs.cpp, particularly regarding OCaml's GC interaction (CAMLparam,CAMLreturn,caml_release_runtime_system).
- Inline Tests:
ppx_inline_test(let%test_unit) withppx_assert([%test_eq:]). Tests live intests/test_runner.ml(a library with(inline_tests)); theparsinglibrary also has(inline_tests)enabled (e.g. the tests at the bottom ofsrc/parsing/astro.ml). - Lwt in tests: Wrap async test bodies in
Lwt_main.run. For testingStrings.parsewithout the filesystem, build an in-memory channel:Lwt_io.of_bytes ~mode:Lwt_io.input (Lwt_bytes.of_string content). - Synchronous parsers:
Js.extract_to_collectorand the HTML/Pug/Astro parsers work on raw strings synchronously — no Lwt needed in those tests. - Integration Tests:
tests/dunecontains a bash-basedruntestrule using fixtures intests/fixtures/(demo.html, demo.js, demo.pug, demo.vue, demo.astro). The CI workflow (.github/workflows/test.yml) requiresmkdir -p stringsbeforedune runtest tests/.
If you receive an error stating that a file has been "modified since it was last read", it usually indicates a discrepancy between the file's filesystem timestamp and the internal state tracking.
Example Error:
Edit failed: The file '/path/to/file' was modified since it was last read. Please read the file again before trying to edit it.
Recommended Fix:
- Execute
touch filenameto reset the file's modification time to the current system time. - Re-read the file using the
viewtool. - Attempt the edit again.