Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
68cbcb6
Refactor Publication: replace DateTimeOffset? DefaultedAt with bool I…
claude Mar 3, 2026
aea0048
Rename "default publication" to "main publication" for consistency
claude Mar 25, 2026
7867d57
Add Publication.IsMain migration; bump dotnet-ef to EF Core 10
myieye Jun 8, 2026
a76259b
Enforce single main-publication invariant and converge merged duplicates
myieye Jun 8, 2026
8c0600f
Extract renamed "main publication" i18n string
myieye Jun 8, 2026
86f02a7
Fix viewer type/lint errors from the IsMain refactor
myieye Jun 8, 2026
26dadf2
Remove redundant IsMain stripping when syncing publications to FwData
myieye Jun 8, 2026
9b968c9
Fix FW Lite tests for the auto-added main publication
myieye Jun 8, 2026
f3eef93
Fix dry-run sync failure when adding the main publication
myieye Jun 8, 2026
8b8e773
Regenerate FW Lite verified snapshots for Publication.IsMain
myieye Jun 8, 2026
8accfa4
Make main-publication auto-add opt-in; converge duplicate mains by de…
myieye Jun 17, 2026
f009044
Clarify FwLite conformance-test guidance; trim i18n-completeness agen…
myieye Jun 17, 2026
600c7cc
Refine IsMain: single-main guard in the validation wrapper; require C…
myieye Jun 18, 2026
0d4dcba
Validate the publication before/after update in the wrapper
myieye Jun 18, 2026
176c5d6
Rename createEntryOptions helper to satisfy the naming-convention lint
myieye Jun 18, 2026
fd7fd50
Preserve dated sena-3 snapshot; capture IsMain format as new dated file
myieye Jun 18, 2026
e1ef724
Split sena-3 snapshot test into a latest round-trip target and dated …
myieye Jun 18, 2026
360846a
Re-extract i18n to drop the orphaned "main publication is always incl…
myieye Jun 18, 2026
0e03364
Revert "Split sena-3 snapshot test into a latest round-trip target an…
myieye Jun 19, 2026
694066e
Drop the dotnet-ef tooling bump from this PR
myieye Jun 19, 2026
ef1e98c
Restore the CreateEntryOptions.WithoutComplexFormsAndComponents helper
myieye Jun 19, 2026
cc72724
Let the FwLiteWeb create-entry endpoints take optional CreateEntryOpt…
myieye Jun 19, 2026
5f0173b
Only rebuild the publication patch when an IsMain op is present
myieye Jun 19, 2026
c1a22dd
Diff IsMain in publication sync instead of special-casing it
myieye Jun 19, 2026
3dbadf9
Run the main-publication invariant tests on FwData too
myieye Jun 19, 2026
ac9611f
Drop the misleading half of the EntrySync staging comment
myieye Jun 19, 2026
2cb27a2
Refine IsMain publication tests and validation after review
myieye Jun 19, 2026
077d517
Fix trailing comma in the minimized snapshot regression data
myieye Jun 19, 2026
bbe22ed
Translate IsMain in SubmitUpdatePublication after rebasing onto develop
myieye Jun 22, 2026
77fccaf
add a main dictionary to the example project
hahn-kev Jun 23, 2026
fe987a3
Default new entries to the main publication; let the UI control it
myieye Jun 26, 2026
4d701f1
Validate SubmitUpdatePublication; address review follow-ups
myieye Jun 29, 2026
0996877
Await the main-publication add before saving a new entry
myieye Jun 29, 2026
c97dc5a
Use ValidationException for single-main guards; surface loaded on the…
myieye Jun 29, 2026
c826979
Note the validation-exception convention in FwLite AGENTS.md
myieye Jun 29, 2026
796e730
Fix new-entry dialog firing two getPublications calls and dropping th…
myieye Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 10 additions & 29 deletions .claude/agents/i18n-completeness.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: i18n-completeness
description: Verify the i18n extraction workflow completed end-to-end when new user-visible strings are added in frontend/viewer/**. Extraction freshness, .po file consistency, context comments per I18N_CONTEXT_GUIDE.md. Distinct from viewer-watcher's parser-awareness check.
description: Verify the i18n extraction workflow completed end-to-end when new user-visible strings are added in frontend/viewer/**. Extraction freshness and .po file consistency. Distinct from viewer-watcher's parser-awareness check.
tools: Bash, Read, Grep, Glob
model: sonnet
---
Expand All @@ -19,9 +19,11 @@ detected; nothing to verify" and stop.

## Baseline

Read `frontend/viewer/I18N_CONTEXT_GUIDE.md` for the context-comment
expectations. `frontend/viewer/AGENTS.md` §i18n has the extraction
workflow.
`frontend/viewer/AGENTS.md` §i18n has the extraction workflow.

Translator-context (`#.`) comments are **out of scope** — they're owned
by the `crowdin-merge` skill (via the `i18n-context-writer` agent). Do
not check for, suggest, or flag them, even informationally.

## Standards

Expand All @@ -38,26 +40,7 @@ For each new `$t\`...\`` or `msg\`...\`` introduced by the diff:
Grep `frontend/viewer/src/locales/en.po` for each new string's
msgid form.

### B. Context comments

`I18N_CONTEXT_GUIDE.md` requires context comments on strings that:
- Differ between Classic and Lite views.
- Are ambiguous in isolation ("Open" — a verb? a noun?).
- Contain user-supplied variables.
- Could be confused with similar terms elsewhere.

For each new string, ask: does it need a context comment? If yes and
absent → 💭 nit; suggest one.

Context-comment syntax in source:
```typescript
// i18n: <context>
$t`Open`
```

Or in `msg\`...\`` calls via the `context` option.

### C. Cross-locale state
### B. Cross-locale state

When extraction runs, it adds the new msgid to all `.po` files (as
untranslated). Spot-check:
Expand All @@ -69,14 +52,14 @@ untranslated). Spot-check:
Missing msgid in non-source locales → 💭 nit (Lingui usually syncs them
on next extract; might be a partial run).

### D. Don't translate placeholder strings
### C. Don't translate placeholder strings

If a string only appears in dev / test code (Storybook stories,
`*.test.ts`, dev-only debug routes) → flag as 💭 nit: *"this string is
extracted but only used in dev — wrap in `// i18n-ignore` or scope to
non-extracted context."*

### E. Pluralization & ICU
### D. Pluralization & ICU

Plural forms use Lingui's plural component / API:
```typescript
Expand All @@ -86,7 +69,7 @@ $t`${count, plural, one {# entry} other {# entries}}`
A new user-visible count without pluralization handling → ⚠️ important
*"will read 'You have 1 entries' for count=1"*.

### F. Don't compose translated strings at runtime
### E. Don't compose translated strings at runtime

Even if a translatable phrase looks like literals at extraction time,
concatenating them at runtime breaks grammar in other languages:
Expand All @@ -109,12 +92,10 @@ const label = $t`${noun} modified by ${author}`;
- `src/locales/en.po` — check each new msgid is present.
- `src/locales/*.po` — spot-check other locales were extracted
too.
- `// i18n:` comment density vs new-string density.

## Severity quick map

- New strings without extraction run → ⚠️ important.
- Missing context comment on ambiguous string → 💭 nit.
- Missing plural form on count-bearing string → ⚠️ important.
- Runtime-composed translated string → ⚠️ important.
- Dev-only string extracted → 💭 nit.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Key documentation for this project:
### Testing

- ✅ **DO run unit tests via the CLI**, filtered to the tests relevant to your changes (e.g. `dotnet test backend/FwLite/FwLiteShared.Tests --filter "FullyQualifiedName~MyTestClass"`). Verify tests you wrote or changed actually pass before handing work back. Never run whole suites just to "see if anything broke".
- ✅ **`MiniLcm.Tests`, `LcmCrdt.Tests`, `FwDataMiniLcmBridge.Tests` need no infrastructure** — these are the unit/conformance tests for the two `IMiniLcmApi` implementations; run them filtered to your change like any unit test (this is where a `*TestsBase` change should be verified, including its FwData side). The FwData/LCM-backed selections load a real project (a handful of tests = seconds, a whole class = a minute or two), so keep the filter tight; don't reach for the whole class or project.
- ✅ **FwLite integration tests** (e.g. `FwLiteProjectSync.Tests`) need no infrastructure but are slow. Run a **targeted selection** (specific tests, not necessarily whole classes) when you touched critical sync code **and believe the work is finished** — not on every iteration. Waiting on tests burns time; be deliberate about which runs buy real signal.
- ✅ **`backend/Testing` contains unit tests too** — only tests marked `Category=Integration|FlakyIntegration|RequiresDb` (and the `Testing.Browser` namespace) need infrastructure. Its unit tests are fine to run: `task test:unit -- <filter>` excludes those categories for you.
- ✅ **FwLite viewer Playwright tests MAY be run** — they're cheap: `task playwright-test-standalone -- <test-name-filter>` (from `frontend/viewer/`) auto-starts the vite dev server with the in-browser demo project; no lexbox stack, chromium only. Always filter to relevant tests; details in `frontend/viewer/AGENTS.md`.
Expand Down
6 changes: 6 additions & 0 deletions backend/FwLite/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,12 @@ if (entity?.DeletedAt is not null) return;

---

## Validation

Imperative validation in `MiniLcmApiValidationWrapper` (rules that need an async lookup, so they can't be FluentValidation rules) must throw `FluentValidation.ValidationException` — the same type the validators throw — not `InvalidOperationException`. Otherwise the same rule reports different exception types per code path. Reserve `InvalidOperationException` for genuine "can't happen" data-integrity guards, not user-input validation.

---

## Important Files Quick Reference

| File | Purpose | Risk Level |
Expand Down
25 changes: 23 additions & 2 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,12 +364,25 @@ private Publication FromLcmPossibility(ICmPossibility lcmPossibility)
var possibility = new Publication
{
Id = lcmPossibility.Guid,
Name = FromLcmMultiString(lcmPossibility.Name)
Name = FromLcmMultiString(lcmPossibility.Name),
IsMain = lcmPossibility.IsProtected
};

return possibility;
}

// The Main Dictionary is the publication FieldWorks protects from deletion (IsProtected). LibLCM treats anything
// other than exactly one protected publication as an error; we mirror that for >1 but tolerate 0 so a project
// without a Main Dictionary can still create entries (it just won't auto-add one).
private ICmPossibility? FindMainPublication()
{
var protectedPublications = Publications.PossibilitiesOS.Where(pub => pub.IsProtected).ToArray();
if (protectedPublications.Length > 1)
throw new InvalidOperationException(
$"Expected at most one protected (main) publication but found {protectedPublications.Length}.");
return protectedPublications.FirstOrDefault();
}

public Task<Publication> UpdatePublication(Guid id, UpdateObjectInput<Publication> update)
{
var lcmPublication = GetLcmPublication(id);
Expand Down Expand Up @@ -992,8 +1005,16 @@ public Task<int> GetEntryIndex(Guid entryId, string? query = null, IndexQueryOpt

public async Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options = null)
{
options ??= CreateEntryOptions.Everything;
options ??= CreateEntryOptions.WithMainPublication;
entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id;
if (options.AutoAddMainPublication)
{
var mainPublication = FindMainPublication();
if (mainPublication is not null && entry.PublishIn.All(pub => pub.Id != mainPublication.Guid))
{
entry.PublishIn.Add(FromLcmPossibility(mainPublication));
}
}
try
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ public UpdatePublicationProxy(ICmPossibility lcmPublication, FwDataMiniLcmApi le
_lexboxLcmApi = lexboxLcmApi;
}

public override bool IsMain
{
get => _lcmPublication.IsProtected;
set { } // IsMain is mapped from IsProtected in FW; don't write back
}

public override MultiString Name
{
get => new UpdateMultiStringProxy(_lcmPublication.Name, _lexboxLcmApi);
Expand Down
8 changes: 5 additions & 3 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,18 +311,20 @@ public async Task CanSyncRandomEntries(ApiType? roundTripApiType)
// We expect the final result to be equivalent to this "raw"/untouched, requested state.
var expected = after.Copy();

// Don't auto-add the main publication when staging data: the sync path under test (EntrySync) never does.
var asIsEntryOptions = CreateEntryOptions.AsIs;
if (roundTripApi is not null)
{
// round-tripping ensures we're dealing with realistic data
// (e.g. in fwdata ComplexFormComponents do not have an Id)
before = await roundTripApi.CreateEntry(before);
before = await roundTripApi.CreateEntry(before, asIsEntryOptions);
await roundTripApi.DeleteEntry(before.Id);
after = await roundTripApi.CreateEntry(after);
after = await roundTripApi.CreateEntry(after, asIsEntryOptions);
await roundTripApi.DeleteEntry(after.Id);
}

// before should not be round-tripped here. That's handled above.
await Api.CreateEntry(before);
await Api.CreateEntry(before, asIsEntryOptions);

// act
await EntrySync.SyncFull(before, after, Api);
Expand Down
Loading
Loading