Repo Type Integrity Checkers at Check Time - #1087
Merged
Merged
Conversation
zmoskala
marked this pull request as draft
September 21, 2026 14:22
…nt import rejection go red.
…ributors. Keep the creating-repository guide, factory Javadoc, architecture map, and roadmap aligned with checkers that already run at check time.
Keep RepoTypeRepository as type CRUD and load both checker halves through AssetIntegrityCheckerRepository at check time.
Operators using --repo-type or -it should see that check-time enforcement is the union of both, not repository-stored checkers alone.
…aths. Prove repo checkers still run on an empty type, later imports pick up assign/clear/update, and type-only checkers apply to workbench, localized import, batch import, and pseudolocalization.
Keep repository -it checkers on their own line so operators can see both sources without merging them.
Keep the guides aligned with the separate Repository type checkers line.
A failed GET of the assigned type still prints id, type name, repository checkers, and locales instead of aborting the command.
File import treated rejected checks as "no translation" and inserted a second current-variant row, which hit the unique constraint on the next valid import.
zmoskala
marked this pull request as ready for review
September 24, 2026 15:00
ehoogerbeets
approved these changes
Sep 24, 2026
ehoogerbeets
left a comment
Contributor
There was a problem hiding this comment.
My only comment is a nitpick, so approving this. You can update the code or not. Your choice.
| private static final String VALID_MESSAGE_FORMAT_TARGET_WITH_THREE_DOTS = | ||
| "{numFiles, plural, one{Il y a un fichier...} other{Il y a # fichiers...}}"; | ||
| private static final String BROKEN_MESSAGE_FORMAT_TARGET_ALTERNATE = | ||
| "{numFiles, plural, one{Il y a deux fichiers} other{Il y a # fichiers}"; |
Contributor
There was a problem hiding this comment.
Nitpick
Il y a deux fichiers => There are two files
This is in the singular plural choice.
Use 'Il y a un seul fichier' instead of 'Il y a deux fichiers' in the
one{} branch, keeping it distinct from BROKEN_MESSAGE_FORMAT_TARGET so
the re-import still creates a new variant.
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
IntegrityCheckerFactory.getTextUnitCheckersresolves a set union of type-owned and repository-owned checker types whoseassetExtensionmatches the asset path's extension, so the same(assetExtension, integrityCheckerType)configured in both places is instantiated once, while different checker types on the same extension all run. Untyped repositories keep using only their own-itcheckers.AssetIntegrityCheckerRepositorykeyed by repository id plus asset extension. The check path is reached with detached assets, so resolving throughasset.getRepository().getRepoType()would depend on a lazy association being initialized; the query avoids that and returns an empty set for an untyped repository.repo-viewstays repository-only for-itconfig: type-owned checkers print on a separateRepository type checkers -->line and are never merged intoIntegrity checkers -->. They come from a follow-upRepoTypeClient.getRepoTypeByIdbecause the nestedrepoTypeon the repository payload is still onlyid+name. If that GET fails,repo-viewprintsRepository type checkers --> could not be loadedand still prints the rest of the repository (id, type name,-itcheckers, locales).repo-create/repo-updatehelp now states that-itstores checkers on the repository only and that an assigned type also runs its own.Extra
Not part of the original union work. I ran into this while walking through local manual tests on this PR.
In Test E I created a repo that had
properties:MESSAGE_FORMATboth on the type and on-it, imported a broken ICU string (that part was fine — rejected, English source still in the pull), then imported a valid translation. That second import died withAn unexpected error happenedand a unique-constraint violation ontm_text_unit_current_variant(UK__TM_TEXT_UNIT_ID__LOCALE_ID). Same crash on Test C’s follow-up (untyped repo, only-it), so this is not the type/repo union.What was going on: a failed integrity check still writes a current-variant row (just not included in the localized file). The next file import is supposed to update that row. Instead it treated the locale as empty, because the “are there already translations?” check used the inheritance cache, and that cache ignores rejected strings. So it tried to insert a second current variant for the same text unit + locale.
The change is small: look at actual current-variant rows for that asset and locale, including rejected ones, before skipping the lookup. First import of a locale that truly has none still skips the per-string lookup. Automated coverage:
AssetIntegrityCheckerServiceTest#testLocalizedAssetImportAfterRejectedIntegrityCheckUpdatesCurrentVariant(broken properties import, then a valid one; current variant becomes the good string and is included).Test plan
Manual
Verified against a locally built webapp only — default in-memory HSQL, no extra Spring config, nothing deployed. All CLI calls go through the alias below, which pins the client at
localhost:8080.The thing to watch in almost every test is the pulled French file, not the CLI output. An import that fails an integrity check still says
Finished: the translation is stored, just marked as not includable. So if a checker rejected the target,pullwrites the English source instead, and if nothing rejected it, the broken French shows up in the file.Setup (webapp, CLI alias, fixtures)
Two separate source directories, because
pushuploads every matching file andimportthen expects a localized file for each asset:mkdir -p tmp/integrity-checkers/{src-msg,src-ellipsis,fr,out} cd tmp/integrity-checkers printf '%s\n' 'greeting=Hello {name}' > src-msg/messages.properties printf '%s\n' 'greeting=Bonjour {name}' > fr/messages_fr-FR.good.properties printf '%s\n' 'greeting=Bonjour {name' > fr/messages_fr-FR.broken.properties printf '%s\n' 'ellipsis=Hello {name}…' > src-ellipsis/ellipsis.properties printf '%s\n' 'ellipsis=Bonjour {name}...' > fr/ellipsis_fr-FR.propertiesMESSAGE_FORMATrejects the missing}in{name.ELLIPSISrejects ASCII...when the source uses…. A target that is valid ICU but uses...is what separates the two checkers.There is no CLI flag for a type's checkers, so types are created with
mojito-local apiagainst/api/repo-types. The import/pull loop below is the same everywhere — copy a localized file into the source dir, import, delete it, pull, read the result:Config and help
repo-create --helpandrepo-update --helpboth say-itstores checkers on this repository only, that an assigned type also runs its checkers, and that an empty--repo-typemeans untyped / clears the assignment.greeting=Bonjour {nameand the pull contains that broken target verbatim.-it properties:MESSAGE_FORMAT. The old behavior still works. Same broken import, and the pull falls back togreeting=Hello {name}.The actual new behavior
-it, its type hasproperties:MESSAGE_FORMAT. This is the whole point of the change: the type's checker alone rejects the broken target, andrepo-viewshows it onRepository type checkers -->with noIntegrity checkers -->line.properties:MESSAGE_FORMATon both the type and-it. Overlap is safe: broken target rejected, valid target accepted, no errors. A second run of the same checker would look identical in the pull, so this does not prove the factory built only one instance. (The valid-import half is what surfaced the bug described under Extra; it passes on the fixed build.)MESSAGE_FORMAT+ repoELLIPSIS, then typeELLIPSIS+ repoMESSAGE_FORMAT. In both cases a target that is valid ICU but uses...is rejected — so neither side quietly overrides the other.-it MESSAGE_FORMATstill rejected the broken target.xliff:MESSAGE_FORMAT, the asset ismessages.properties, so nothing applies and the broken target ships. Checkers are matched against the asset's extension, not applied to everything.PATCH /api/repo-types/{id}to give that type a checker → rejected. No repository-level-itchange anywhere in that sequence.Every path that resolves checkers
Bonjour {name}works and savingBonjour {nameraises the integrity warning — so this isn't just a CLI-import code path.POST /api/textunitsBatch. A broken placeholder sent through the batch importer is stored but stays out of the pulled file.pseudoproducedgreeting=⟦萬萬萬 Ήēĺĺō {name} 國國國⟧— the placeholder survives untouched.tm-export/tm-import. Exported the TM, broke the closing brace on the French target, re-imported, and the broken string stayed out of the pull. Notetm-exportwrites both a source and a_fr-FRXLIFF;tm-importhas to point at a directory holding only the localized one.Not covered here: any non-local server, a CLI for editing a type's checkers (REST only in this PR), drop-based Project Request import, the role-based permissions on
PATCH /api/repo-types, and counting checker invocations from a pulled file.Automated
./mvnw -pl webapp -am -Dtest=AssetIntegrityCheckerServiceTest -Dsurefire.failIfNoSpecifiedTests=false spotless:check testAssetIntegrityCheckerServiceTest(ServiceTestBase, real DB): untyped repository with no checkers, type-only, repo-only, and overlapping the same pair (import still rejects — not a count of instances); disjoint pairs in both directions (type checker rejects what the repo checker would allow, and the reverse); a typed repository whose type has an empty checker set still runs its own checkers; assigning a type, clearing it, and editing a type's checkers between two imports of the same text unit; and the workbench check, localized-asset import, and pseudolocalization paths driven by a type-only checker. Also: after a rejected localized-asset import, a later valid import updates the current variant instead of 500’ing (testLocalizedAssetImportAfterRejectedIntegrityCheckUpdatesCurrentVariant).AssetIntegrityCheckerServiceTest#testFactoryUnionsCheckersUsingDetachedAssetAndFiltersByExtensionis the “instantiated once” check:MESSAGE_FORMATis configured on both the type and the repository, plus two other checkers, and the factory returns three instances (not four). Same test asserts extension filtering and that the lazyrepoTypeon a detached asset is not initialized first.TextUnitBatchImporterServiceTest#testTypeOnlyIntegrityCheckerIsUsedOnBatchImport: a type-ownedMESSAGE_FORMATexcludes a broken placeholder on batch import, so the union applies toasyncImportTextUnitsand not just the XLIFF path.RepoViewCommandTest(CLITestBase, real HTTP):Repository type checkersappears only when the assigned type has checkers, is absent for an untyped repository and for a type with none, and a repository-itchecker added afterwards prints onIntegrity checkerswithout the two lines collapsing into one.RepoViewTypeCheckersLoadFailureCommandTest: if loading type checkers fails,repo-viewprintscould not be loadedand still prints id, type name, and locales.RepoCreateCommandTest#testCreateHelpDocumentsTypeIntegrityUnion: help documents both halves of the contract —-itstores repository checkers only, and an assigned type also runs its checkers — so dropping either sentence fails the test.