Dictionaries: management commands and the translation round-trip - #44
Open
mateuscardosodeveloper wants to merge 7 commits into
Open
mateuscardosodeveloper wants to merge 7 commits into
mateuscardosodeveloper wants to merge 7 commits into
Conversation
Interactive autocomplete over the profile's dictionaries, mirroring pickEntityIDFromTagoIO so the upcoming dict-* commands share the prompt UX of the device, entity and action families. Labels each choice "name [slug]": a dictionary carries both, and the slug is the meaningful secondary identifier since it is what languageInfoBySlug takes. The label builder is exported so it can be tested directly — the module calls prompts(...) as a function, which a spy on prompts.prompt never intercepts.
entity-data.ts and entity-schema.ts each carried a private, identical copy of parseJSON. The dictionary commands need the same logic, so this extracts it rather than adding a third copy. The helper also takes the array/object discrimination the action builders needed, and a code override so the entity call sites keep reporting json_parse_failed — the code their tests already assert.
Pure assembly and validation for the language payload: reading a JSON
file, parsing repeatable --set pairs, merging the two, and diffing
against the current content to drive the replace confirmation.
Encodes three API constraints that are absent from the SDK types and
from the docs, each verified against a live profile:
- a slug is uppercase alphanumeric, at most 7 characters
- a content key is uppercase letters, digits and underscores, at
least 2 characters
- a locale is well-formed (shape only; a closed list would need
product input)
The API reports the key rule without naming the offending key, so the
offline check names it — a translation file can hold hundreds of
entries.
readContentFile guards with existsSync before reading. entity-create.ts
reads a user-supplied path with a bare readFileSync, so a wrong path
escapes as an unhandled ENOENT with a stack trace instead of a CLI
error; that is not repeated here.
Read-only surface. dict-list collapses the languages array to its codes in human mode, since a table cell renders an object as [object Object]. dict-info renders its human view entirely on stderr. console.table writes to stdout, which is reserved for --json — the leak that shipped in action-info and was caught only by a functional test. dict-list keeps console.table because there the table is the command's data output, matching device-list and entity-list. The API omits id from the dictionaries.info payload, unlike actions and devices, so --json fills it in from the id that was requested. Otherwise a machine reader has no way to identify what it just read, and the human view prints [undefined].
dict-create reads the new id from the SDK's { dictionary } response
rather than { id }. This is the third distinct id key in the codebase,
after devices' { device_id } and actions' { action }, and reading the
wrong one yields undefined ids in --json output.
Name, slug and fallback are all required by the API; each prompts when
omitted and fails under --silent. Slug and locale are validated offline,
so a malformed value never costs a request.
dict-edit builds a partial patch and rejects an empty one with
no_changes before calling the API. dict-delete confirms unless -y or
--silent, naming the language count so the scope of the deletion is
visible, and a declined prompt exits 0 without a request.
One command with mutually-exclusive mode flags, following entity-data and entity-schema: no flag reads, --file/--set writes, --delete removes. Separate commands are the convention for top-level resources; a language lives inside a dictionary. Writing replaces the whole language, mirroring the API's PUT. --merge reads the current content first and layers the payload on top. Without it, a replace that would drop keys confirms with the counts, so a --set ONE_KEY=x cannot silently wipe a translation. The read path swallows a missing-locale error rather than surfacing it: a language only exists once languageEdit writes to it, so the first write to any new locale necessarily reads one the API has never seen. --fallback defaults off on the id route, so a translation export shows only what the locale itself owns. The slug route forces it on — the SDK coerces fallback: false to true there — and --help says so.
Wires the family into the program, which also puts it in the man page through buildProgram. Regenerates the snapshot and extends the command-surface regression guard with dict-list. The --help text carries the behaviours that are invisible from the flags alone: that writing replaces the whole language and --merge is how to keep the rest, the export-translate-import round-trip, the slug's uppercase-alphanumeric rule, and that the --slug read route always applies the fallback.
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a
dict-*command family covering Dictionaries, the multi-language string store behind TagoRUN portals and dashboards. A translation could previously only be edited in the web admin, one string at a time, so translation work was unversionable: there was no way to export a language, hand it to a translator, review the diff in git, and import it back.Six commands:
dict-list,dict-info,dict-create,dict-edit,dict-delete, anddict-langfor the language content inside a dictionary. All carry the family conventions established by PR #43:--jsonwith parseable error codes,--silentfor non-interactive callers, an interactive picker when the ID is omitted, and confirmation on destructive paths.Stacked on #43 — that branch is its base, since both touch
src/index.tsand the man snapshot.The translation round-trip
dict-langis one command with mutually-exclusive mode flags rather than three separate commands, followingentity-dataandentity-schema: separate commands are the convention for top-level resources, and a language lives inside a dictionary.Writing replaces the whole language, mirroring the API's PUT.
--mergekeeps the keys a payload omits. Without it, a replace that would drop keys confirms with the counts first, so a stray--set ONE_KEY=xcannot silently wipe a translation.API constraints encoded
Three rules absent from the SDK types and the docs, each found by probing a live profile and now checked offline:
The API reports the key rule without naming the offending key. Since a translation file can hold hundreds of entries, the offline check names it.
Notes on the SDK
dictionaries.createresolves{ dictionary: "<id>" }— the third distinct id key in this codebase, after devices'{ device_id }and actions'{ action }.dictionaries.infoomitsidentirely, unlike actions and devices, sodict-info --jsonfills it in from the id that was requested.Reading a locale the API has never seen fails, and a language only exists once
languageEditwrites to it, so the first write to any new locale necessarily reads a missing one.dict-langswallows that specific failure; without it--mergeand the replace diff could not work on a new language.Shared helper extracted
entity-data.tsandentity-schema.tseach carried a private, identical copy ofparseJSON. Rather than add a third, it moves tosrc/lib/parse-json-flag.tsand both migrate to it. Their call sites and thejson_parse_failedcode are unchanged.Test plan
npm test);commands/dictionariesabove 90% on every filenpx tsc --noEmitclean,npm run linter0 errors,oxfmt --checkcleanparseJSONextraction (79 assertions, unchanged).SSentries, no removals, noHeaderleak). Regression guard extended withdict-list--mergepreserves keys absent from the payload; without it a key-dropping replace prompts with the counts and declining changes nothing--slugread route returns the same content as the id route--inactivemarks a language inactive anddict-inforeflects itdict-infoanddict-langwrite nothing to stdout in human modeRisk (CIA)
Likelihood: 🟢 Low | Impact: 🟡 Medium | Exposure: 🟢 Low
Warning
Impact is Medium because
dict-deleteremoves a dictionary and every translation in it, and adict-langwrite replaces a language wholesale by default. Deletion confirms unless-yor--silentis explicit, naming the language count so the scope is visible. A replace that would drop keys confirms with the added/removed counts, and a declined prompt exits 0 without a request. TheparseJSONextraction touches two entity commands, but their call sites, error code and tests are unchanged.