Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dbba654
Run repo type integrity checkers at runtime
zmoskala Sep 21, 2026
fd73cc9
Tighten integrity-checker union tests so type contribution and disjoi…
zmoskala Sep 22, 2026
9dfbc72
Describe type-and-repo integrity checker union as current behavior.
zmoskala Sep 22, 2026
2a4ca6c
Fix Spotless indentation on RepoType integrity-checker Javadoc.
zmoskala Sep 22, 2026
97b2881
Document type-and-repo integrity checker union for operators and cont…
zmoskala Sep 22, 2026
5d93f77
Move type-checker lookup next to repository checker resolution.
zmoskala Sep 22, 2026
7de2240
Warn in CLI help that assigning a repo type also runs type checkers.
zmoskala Sep 22, 2026
211f88b
Cover empty type checkers, live assignment changes, and other check p…
zmoskala Sep 22, 2026
1e61f06
Show type-owned integrity checkers on repo-view.
zmoskala Sep 23, 2026
fdba878
Document repo-view listing type-owned integrity checkers.
zmoskala Sep 23, 2026
fab8c10
Apply Spotless formatting to repo type checker code.
zmoskala Sep 23, 2026
ff7124e
Cover type-checker rejection when a disjoint repo checker would allow.
zmoskala Sep 23, 2026
cdcab82
Keep repo-view running when type checkers cannot be loaded.
zmoskala Sep 23, 2026
2e000b1
Look up existing current variants after a rejected integrity import.
zmoskala Sep 24, 2026
cbf2473
Wrap repo-view Javadoc to satisfy Spotless.
zmoskala Sep 24, 2026
6989937
Rename factory locals so type vs repository checkers are obvious.
zmoskala Sep 24, 2026
4c9d476
Keep the original @author on AssetIntegrityCheckerRepository.
zmoskala Sep 24, 2026
c17af3f
Fix singular plural branch in alternate broken MessageFormat test target
zmoskala Sep 25, 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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public abstract class RepoCommand extends Command {
protected static final String INTEGRITY_CHECK_SHORT_PARAM = "-it";
protected static final String INTEGRITY_CHECK_DESCRIPTION =
"Integrity Checker by File Extension, comma seperated format: \"FILE_EXTENSION_1:CHECKER_TYPE_1,FILE_EXTENSION_2:CHECKER_TYPE_2\"\n "
+ "Stores checkers on this repository only. If a repository type is assigned, Mojito also runs that type's checkers.\n "
+ "Available Checker types: MESSAGE_FORMAT, MESSAGE_FORMAT_DOUBLE_BRACES, PRINTF_LIKE, PRINTF_LIKE_IGNORE_PERCENTAGE_AFTER_BRACKETS, PRINTF_LIKE_VARIABLE_TYPE, PRINTF_LIKE_ADD_PARAMETER_SPECIFIER, SIMPLE_PRINTF_LIKE, COMPOSITE_FORMAT, WHITESPACE, TRAILING_WHITESPACE, HTML_TAG, ELLIPSIS, BACKQUOTE\n "
+ "For examples: \"properties:MESSAGE_FORMAT,xliff:PRINTF_LIKE\"";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.box.l10n.mojito.cli.command.param.Param;
import com.box.l10n.mojito.rest.client.RepoTypeClient;
import com.box.l10n.mojito.rest.client.exception.RepositoryNotFoundException;
import com.box.l10n.mojito.rest.entity.IntegrityChecker;
import com.box.l10n.mojito.rest.entity.RepoType;
import com.box.l10n.mojito.rest.entity.RepoTypeIntegrityChecker;
import com.box.l10n.mojito.rest.entity.Repository;
import com.box.l10n.mojito.rest.entity.RepositoryLocale;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.fusesource.jansi.Ansi;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;

/**
* Command to view properties of existing repository
Expand All @@ -31,6 +37,8 @@ public class RepoViewCommand extends RepoCommand {
/** logger */
static Logger logger = LoggerFactory.getLogger(RepoViewCommand.class);

@Autowired RepoTypeClient repoTypeClient;

@Parameter(
names = {Param.REPOSITORY_NAME_LONG, Param.REPOSITORY_NAME_SHORT},
arity = 1,
Expand All @@ -52,6 +60,7 @@ public void execute() throws CommandException {
.println();
printRepoType(repository);
printIntegrityChecker(repository);
printRepoTypeIntegrityCheckers(repository);
printLocales(repository);
consoleWriter.println();
} catch (RepositoryNotFoundException ex) {
Expand All @@ -76,18 +85,72 @@ private void printIntegrityChecker(Repository repository) {
integrityCheckers.addAll(repository.getIntegrityCheckers());
Collections.sort(integrityCheckers, IntegrityChecker.getComparator());

consoleWriter.newLine().a("Integrity checkers --> ").fg(Ansi.Color.MAGENTA);
for (int i = 0; i < integrityCheckers.size(); i++) {
IntegrityChecker integrityChecker = integrityCheckers.get(i);
consoleWriter
.a(integrityChecker.getAssetExtension())
.a(":")
.a(integrityChecker.getIntegrityCheckerType().toString());
if (i == integrityCheckers.size() - 1) {
consoleWriter.println();
} else {
consoleWriter.a(",");
}
List<String> pairs = new ArrayList<>();
for (IntegrityChecker integrityChecker : integrityCheckers) {
pairs.add(
integrityChecker.getAssetExtension()
+ ":"
+ integrityChecker.getIntegrityCheckerType().toString());
}
printCheckerLine("Integrity checkers --> ", pairs);
}
}

/**
* Prints type-owned checkers from a follow-up {@code GET /api/repo-types/{id}}. Nested {@code
* repoType} on the repository payload is only {@code id} and {@code name}. A failed fetch must
* not abort {@code repo-view}; the rest of the repository is still printed.
*/
private void printRepoTypeIntegrityCheckers(Repository repository) {
if (repository.getRepoType() == null || repository.getRepoType().getId() == null) {
return;
}

RepoType repoType;
try {
repoType = repoTypeClient.getRepoTypeById(repository.getRepoType().getId());
} catch (RestClientException ex) {
logger.debug("Could not load repository type checkers", ex);
consoleWriter
.newLine()
.a("Repository type checkers --> ")
.fg(Ansi.Color.YELLOW)
.a("could not be loaded")
.println();
return;
}

if (repoType.getIntegrityCheckers() == null || repoType.getIntegrityCheckers().isEmpty()) {
return;
}

List<RepoTypeIntegrityChecker> typeCheckers = new ArrayList<>(repoType.getIntegrityCheckers());
typeCheckers.sort(
Comparator.comparing(
RepoTypeIntegrityChecker::getAssetExtension,
Comparator.nullsLast(String::compareTo))
.thenComparing(
checker ->
checker.getIntegrityCheckerType() == null
? ""
: checker.getIntegrityCheckerType().name()));

List<String> pairs = new ArrayList<>();
for (RepoTypeIntegrityChecker typeChecker : typeCheckers) {
pairs.add(
typeChecker.getAssetExtension() + ":" + typeChecker.getIntegrityCheckerType().toString());
}
printCheckerLine("Repository type checkers --> ", pairs);
}

private void printCheckerLine(String label, List<String> pairs) {
consoleWriter.newLine().a(label).fg(Ansi.Color.MAGENTA);
for (int i = 0; i < pairs.size(); i++) {
consoleWriter.a(pairs.get(i));
if (i == pairs.size() - 1) {
consoleWriter.println();
} else {
consoleWriter.a(",");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ public class Param {

public static final String REPOSITORY_TYPE_LONG = "--repo-type";
public static final String REPOSITORY_TYPE_DESCRIPTION =
"Exact name of the repository type to assign. An empty value leaves the repository untyped"
+ " on create and clears the assignment on update";
"Exact name of the repository type to assign. Assigned types also run their integrity"
+ " checkers at check time, unioned with this repository's -it checkers. An empty value"
+ " leaves the repository untyped on create and clears the assignment on update";

public static final String REPOSITORY_LOCALES_LONG = "--locales";
public static final String REPOSITORY_LOCALES_SHORT = "-l";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ public void testCreateRepositoryWithRepoType() throws Exception {
assertEquals(repoType.getId(), repository.getRepoType().getId());
}

@Test
public void testCreateHelpDocumentsTypeIntegrityUnion() {
getL10nJCommander().run("repo-create", "-h");

String output = outputCapture.toString();
assertTrue(output.contains(Param.REPOSITORY_TYPE_LONG));
assertTrue(
"CLI help must warn that assigning a type also runs type integrity checkers",
output.contains("Assigned types also run") && output.contains("their integrity checkers"));
assertTrue(output.contains(RepoCommand.INTEGRITY_CHECK_SHORT_PARAM));
assertTrue(
"CLI help must say -it stores repository checkers only",
output.contains("Stores checkers on this repository only"));
}

@Test
public void testCreateRepositoryWithEmptyRepoTypeStaysUntyped() throws Exception {
String repositoryName = testIdWatcher.getEntityName("emptyTypeRepository");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import com.box.l10n.mojito.cli.CLITestBase;
import com.box.l10n.mojito.cli.command.param.Param;
import com.box.l10n.mojito.entity.RepoType;
import com.box.l10n.mojito.entity.RepoTypeIntegrityChecker;
import com.box.l10n.mojito.entity.Repository;
import com.box.l10n.mojito.service.assetintegritychecker.integritychecker.IntegrityCheckerType;
import com.box.l10n.mojito.service.repository.RepositoryRepository;
import com.box.l10n.mojito.service.repotype.RepoTypeService;
import java.util.Set;
Expand Down Expand Up @@ -38,6 +40,7 @@ public void testViewTestRepo() throws Exception {
"Repository id is missing or incorrect from output",
outputCapture.toString().contains("Repository id --> " + repository.getId()));
assertFalse(outputCapture.toString().contains("Repository type -->"));
assertFalse(outputCapture.toString().contains("Repository type checkers -->"));
assertFalse(
"Repository integrity checker is incorrect",
outputCapture.toString().contains("Integrity checkers -->"));
Expand Down Expand Up @@ -78,6 +81,46 @@ public void testViewRepoType() throws Exception {
getL10nJCommander().run("repo-view", Param.REPOSITORY_NAME_SHORT, repository.getName());

assertTrue(outputCapture.toString().contains("Repository type --> " + repoType.getName()));
assertFalse(outputCapture.toString().contains("Repository type checkers -->"));
}

@Test
public void testViewRepoTypeIntegrityCheckers() throws Exception {
Repository repository = createTestRepoUsingRepoService();
String testRepoName = repository.getName();

RepoTypeIntegrityChecker typeChecker = new RepoTypeIntegrityChecker();
typeChecker.setAssetExtension("xliff");
typeChecker.setIntegrityCheckerType(IntegrityCheckerType.SIMPLE_PRINTF_LIKE);

RepoType repoType =
repoTypeService.createRepoType(
testIdWatcher.getEntityName("React"), null, null, Set.of(typeChecker));
repository.setRepoType(repoType);
repositoryRepository.save(repository);

getL10nJCommander().run("repo-view", Param.REPOSITORY_NAME_SHORT, testRepoName);

String output = outputCapture.toString();
assertTrue(output.contains("Repository type --> " + repoType.getName()));
assertTrue(output.contains("Repository type checkers --> xliff:SIMPLE_PRINTF_LIKE"));
assertFalse(output.contains("Integrity checkers -->"));

getL10nJCommander()
.run(
"repo-update",
Param.REPOSITORY_NAME_SHORT,
testRepoName,
RepoCommand.INTEGRITY_CHECK_SHORT_PARAM,
"properties:MESSAGE_FORMAT");
getL10nJCommander().run("repo-view", Param.REPOSITORY_NAME_SHORT, testRepoName);

output = outputCapture.toString();
assertTrue(output.contains("Integrity checkers --> properties:MESSAGE_FORMAT"));
assertTrue(output.contains("Repository type checkers --> xliff:SIMPLE_PRINTF_LIKE"));
assertFalse(
output.contains(
"Integrity checkers --> properties:MESSAGE_FORMAT,xliff:SIMPLE_PRINTF_LIKE"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.box.l10n.mojito.cli.command;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.box.l10n.mojito.cli.console.ConsoleWriter;
import com.box.l10n.mojito.rest.client.RepoTypeClient;
import com.box.l10n.mojito.rest.client.RepositoryClient;
import com.box.l10n.mojito.rest.entity.Locale;
import com.box.l10n.mojito.rest.entity.RepoType;
import com.box.l10n.mojito.rest.entity.Repository;
import com.box.l10n.mojito.rest.entity.RepositoryLocale;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.springframework.boot.test.system.OutputCaptureRule;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.client.HttpClientErrorException;

/**
* {@code repo-view} {@code execute()} against stubbed clients (no JCommander, no HTTP). Covers
* degrading when type checkers cannot be loaded.
*/
public class RepoViewTypeCheckersLoadFailureCommandTest {

@Rule public OutputCaptureRule outputCapture = new OutputCaptureRule();

RepositoryClient repositoryClient;
RepoTypeClient repoTypeClient;
RepoViewCommand command;

@Before
public void setUp() {
repositoryClient = mock(RepositoryClient.class);
repoTypeClient = mock(RepoTypeClient.class);
command = new RepoViewCommand();
command.repositoryClient = repositoryClient;
command.repoTypeClient = repoTypeClient;
command.consoleWriter =
new ConsoleWriter(false, ConsoleWriter.OutputType.ANSI_CONSOLE_AND_LOGGER);
command.nameParam = "demo";
}

@Test
public void printsNoteAndContinuesWhenTypeCheckersCannotBeLoaded() throws Exception {
Repository repository = typedRepository();
when(repositoryClient.getRepositoryByName("demo")).thenReturn(repository);
when(repoTypeClient.getRepoTypeById(9L))
.thenThrow(
HttpClientErrorException.create(
HttpStatus.NOT_FOUND,
HttpStatus.NOT_FOUND.getReasonPhrase(),
HttpHeaders.EMPTY,
new byte[0],
StandardCharsets.UTF_8));

command.execute();

String output = outputCapture.toString();
assertTrue(output.contains("Repository id --> 1"));
assertTrue(output.contains("Repository type --> React"));
assertTrue(output.contains("Repository type checkers --> could not be loaded"));
assertTrue(output.contains("Repository locales --> fr-FR"));
assertFalse(output.contains("Integrity checkers -->"));
}

private static Repository typedRepository() {
RepoType repoType = new RepoType();
repoType.setId(9L);
repoType.setName("React");

Locale frFR = new Locale();
frFR.setBcp47Tag("fr-FR");
RepositoryLocale repositoryLocale = new RepositoryLocale();
repositoryLocale.setLocale(frFR);
repositoryLocale.setToBeFullyTranslated(true);

Repository repository = new Repository();
repository.setId(1L);
repository.setName("demo");
repository.setRepoType(repoType);
repository.setRepositoryLocales(Set.of(repositoryLocale));
return repository;
}
}
19 changes: 10 additions & 9 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ Items are ordered roughly by dependency where that applies; ordering within a to

Core Mojito orchestration for AI-assisted translation.

Integrity checkers configured on a repository type already run at check time for assigned repositories (unioned with any repository-owned checkers). CLI and UI for managing those type checkers is still follow-up.

1. Add Repo Types
2. Prompt Layer per Repo Type
3. Prompt Layer per Repo
4. Integrity checks per Repo Type
5. Glossary awareness in AI translation flows (approved terms in prompts)
6. Derived-locale AI adaptation (parent → regional variant)
7. Derived-locale prompt layers
8. Fuzzy match support using Lucene search
9. Translation memory support using fuzzy match (TM hits as AI reference context)
10. Workbench free-text / TM search (filters by repo, locale, status)
11. MQM-style quality measurement (segment-level scoring, batch scorecards, rollups)
12. Automatic translation within Mojito (triggered on push / status changes), instead of kicking off translations externally via the CLI
4. Glossary awareness in AI translation flows (approved terms in prompts)
5. Derived-locale AI adaptation (parent → regional variant)
6. Derived-locale prompt layers
7. Fuzzy match support using Lucene search
8. Translation memory support using fuzzy match (TM hits as AI reference context)
9. Workbench free-text / TM search (filters by repo, locale, status)
10. MQM-style quality measurement (segment-level scoring, batch scorecards, rollups)
11. Automatic translation within Mojito (triggered on push / status changes), instead of kicking off translations externally via the CLI

### A Few Definitions

Expand Down
22 changes: 20 additions & 2 deletions docs/_docs/guides/003-creating-repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ categories: guides
permalink: /docs/guides/creating-repository/
---

In this guide, we use `mojito-cli` to create a repository in {{ site.mojito_green }}. Repository is a container for strings and their translations. It also stores localization configurations such as locales and integrity checkers.
In this guide, we use `mojito-cli` to create a repository in {{ site.mojito_green }}. Repository is a container for strings and their translations. It also stores localization configurations such as locales, an optional repository type, and integrity checkers.


### Creating Repository
Expand Down Expand Up @@ -34,7 +34,25 @@ You can now see the repository `MyRepo` in the Webapp.
This creates a repository with name `MyRepo` with locales `fr-FR`.


The repository is configured to use `COMPOSITE_FORMAT` integrity checker for files with `resw` extension. Integrity checkers are used to validate translations. Integrity checker configuration is optional but it is highly recommended to catch translations with errors and reject them. See [Integrity Checkers]({{ site.url }}/docs/guides/integrity-checkers/) for more information.
The repository is configured to use `COMPOSITE_FORMAT` integrity checker for files with `resw` extension. Integrity checkers are used to validate translations. Integrity checker configuration is optional but it is highly recommended to catch translations with errors and reject them. See [Integrity Checkers]({{ site.url }}/docs/guides/integrity-checkers/) for more information. If the repository is also assigned to a repository type, Mojito also runs that type's checkers; see [Creating Repository with a Repository Type](#creating-repository-with-a-repository-type).


### Creating Repository with a Repository Type

mojito repo-create -n MyRepo --repo-type React -l fr-FR


This creates a repository named `MyRepo` assigned to the existing repository type `React`. Omitting `--repo-type`, or passing an empty value, creates an untyped repository.


Assigning a type does not replace repository-level `-it` checkers. At check time Mojito runs the union of the type's integrity checkers and the repository's own checkers for each file extension. You can also assign or change the type later, or clear it:

mojito repo-update -n MyRepo --repo-type React

mojito repo-update -n MyRepo --repo-type ""


`repo-view` prints `Repository type --> React` when a type is assigned. Type-owned integrity checkers appear on a separate `Repository type checkers` line; repository `-it` checkers stay on `Integrity checkers`. See [Integrity Checkers]({{ site.url }}/docs/guides/integrity-checkers/).



Expand Down
2 changes: 2 additions & 0 deletions docs/_docs/guides/009-integrity-checkers.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ In this guide, let's go over the integrity checkers in {{ site.mojito_green }} i

We use `mojito-cli` to configure integrity checkers in a repository. Integrity checkers can be configured when you create and update repository in {{ site.mojito_green }} with `-it` parameter. You can set integrity checker for each file extension of resource files. For example, `-it resw:COMPOSITE_FORMAT,xlf:PRINTF_LIKE`.

When a repository is assigned to a repository type, checks also include the integrity checkers configured on that type. Mojito runs the union of type and repository checkers for each file extension, so the same `(extension, checker type)` configured in both places runs only once. `repo-view` lists repository-stored checkers on `Integrity checkers` (configured with `-it`) and type-owned checkers on `Repository type checkers`.

```bash
mojito repo-create -n MyRepo -it "properties:MESSAGE_FORMAT" -l de-DE es-ES

Expand Down
Loading
Loading