Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions src/main/java/com/crowdin/cli/client/CrowdinProjectClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,25 @@ public List<SourceString> listSourceString(Long fileId, Long branchId, String la
.listSourceStrings(this.projectId, builder.limit(limit).offset(offset).build()));
}

@Override
@SneakyThrows
public void batchEditSourceStrings(List<PatchRequest> request) {
Map<BiPredicate<String, String>, ResponseException> errorHandler = new LinkedHashMap<>() {{
put((code, message) -> message.contains("Someone else is currently editing one of the file"),
new RepeatException("Someone else is currently editing one of the file."));
put((code, message) -> code.equals("409"),
new RepeatException("Conflict occurred while batch editing source strings. Please try again."));
}};
executeRequestWithPossibleRetries(
errorHandler,
() -> this.client.getSourceStringsApi()
.stringBatchOperations(this.projectId, request)
.getData(),
3,
3 * 1000
);
}

@Override
public void deleteSourceString(Long sourceId) {
executeRequest(() -> {
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/crowdin/cli/client/ProjectClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ default CrowdinProjectFull downloadFullProject() {

List<SourceString> listSourceString(Long fileId, Long branchId, String labelIds, String filter, String croql, Long directory, String scope);

void batchEditSourceStrings(List<PatchRequest> request);

void deleteSourceString(Long id);

StringComment commentString(AddStringCommentRequest request);
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/crowdin/cli/commands/Actions.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.io.File;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -164,4 +165,32 @@ NewAction<PropertiesWithFiles, ProjectClient> preTranslate(
NewAction<ProjectProperties, ProjectClient> uninstallApp(String id, Boolean force);

NewAction<ProjectProperties, ProjectClient> installApp(String identifier);

NewAction<ProjectProperties, ProjectClient> contextDownload(
File to,
List<String> filesFilter,
List<String> labelsFilter,
String branchFilter,
String croqlFilter,
LocalDate sinceFilter,
String statusFilter,
String outputFormat,
FilesInterface files,
boolean plainView,
boolean noProgress
);

NewAction<ProjectProperties, ProjectClient> contextUpload(File file, boolean overwrite, boolean dryRun, boolean plainView, int batchSize);

NewAction<ProjectProperties, ProjectClient> contextReset(
List<String> filesFilter,
List<String> labelsFilter,
String branchFilter,
String croqlFilter,
LocalDate sinceFilter,
boolean dryRun,
int batchSize,
boolean plainView,
boolean noProgress
);
}
16 changes: 16 additions & 0 deletions src/main/java/com/crowdin/cli/commands/actions/CliActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import java.io.File;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -358,4 +359,19 @@ public NewAction<ProjectProperties, ProjectClient> uninstallApp(String id, Boole
public NewAction<ProjectProperties, ProjectClient> installApp(String identifier) {
return new AppInstallAction(identifier);
}

@Override
public NewAction<ProjectProperties, ProjectClient> contextDownload(File to, List<String> filesFilter, List<String> labelsFilter, String branchFilter, String croqlFilter, LocalDate sinceFilter, String statusFilter, String outputFormat, FilesInterface files, boolean plainView, boolean noProgress) {
return new ContextDownloadAction(to, filesFilter, labelsFilter, branchFilter, croqlFilter, sinceFilter, statusFilter, outputFormat, files, plainView, noProgress);
}

@Override
public NewAction<ProjectProperties, ProjectClient> contextUpload(File file, boolean overwrite, boolean dryRun, boolean plainView, int batchSize) {
return new ContextUploadAction(file, overwrite, dryRun, plainView, batchSize);
}

@Override
public NewAction<ProjectProperties, ProjectClient> contextReset(List<String> filesFilter, List<String> labelsFilter, String branchFilter, String croqlFilter, LocalDate sinceFilter, boolean dryRun, int batchSize, boolean plainView, boolean noProgress) {
return new ContextResetAction(filesFilter, labelsFilter, branchFilter, croqlFilter, sinceFilter, dryRun, batchSize, plainView, noProgress);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.crowdin.cli.commands.actions;

import com.crowdin.cli.client.CrowdinProjectFull;
import com.crowdin.cli.client.ProjectClient;
import com.crowdin.cli.commands.NewAction;
import com.crowdin.cli.commands.Outputter;
import com.crowdin.cli.commands.functionality.FilesInterface;
import com.crowdin.cli.properties.ProjectProperties;
import com.crowdin.cli.utils.AiContextUtil;
import com.crowdin.cli.utils.GlobUtil;
import com.crowdin.cli.utils.StringUtil;
import com.crowdin.cli.utils.Utils;
import com.crowdin.cli.utils.console.ConsoleSpinner;
import com.crowdin.client.labels.model.Label;
import com.crowdin.client.projectsgroups.model.Type;
import com.crowdin.client.sourcefiles.model.Branch;
import com.crowdin.client.sourcefiles.model.FileInfo;
import com.crowdin.client.sourcestrings.model.SourceString;
import lombok.AllArgsConstructor;
import org.json.JSONObject;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
import java.util.stream.Collectors;

import static com.crowdin.cli.BaseCli.RESOURCE_BUNDLE;
import static com.crowdin.cli.utils.console.ExecutionStatus.OK;
import static com.crowdin.cli.utils.console.ExecutionStatus.WARNING;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.nonNull;

@AllArgsConstructor
class ContextDownloadAction implements NewAction<ProjectProperties, ProjectClient> {

private final File to;
private final List<String> filesFilter;
private final List<String> labelsFilter;
private final String branchFilter;
private final String croqlFilter;
private final LocalDate sinceFilter;
private final String statusFilter;
private final String outputFormat;
private final FilesInterface files;
private final boolean plainView;
private final boolean noProgress;

@Override
public void act(Outputter out, ProjectProperties pb, ProjectClient client) {

CrowdinProjectFull project = ConsoleSpinner.execute(
out,
"message.spinner.fetching_project_info",
"error.collect_project_info",
this.noProgress,
this.plainView,
() -> client.downloadFullProject(this.branchFilter)
);

List<AiContextUtil.StringContextRecord> existingRecords = to.exists() ? AiContextUtil.readRecords(to) : List.of();

boolean isStringsBasedProject = Objects.equals(project.getType(), Type.STRINGS_BASED);

Long branchId = Optional.ofNullable(project.getBranch())
.map(Branch::getId)
.orElse(null);

Map<String, Long> labelsMap = client.listLabels().stream()
.collect(Collectors.toMap(Label::getTitle, Label::getId, (existing, replacement) -> existing));

String filterLabelsStr = Optional
.ofNullable(this.labelsFilter)
.filter(list -> !list.isEmpty())
.map(list -> list.stream()
.filter(labelsMap::containsKey)
.map(label -> labelsMap.get(label).toString())
.collect(Collectors.joining(",")))
.orElse(null);

String encodedCroql = nonNull(croqlFilter) ? Utils.encodeURL(croqlFilter) : null;

List<Long> fileIds = null;

if (!isStringsBasedProject && nonNull(this.filesFilter) && !this.filesFilter.isEmpty()) {
fileIds = project.getFileInfos().stream()
.filter(file -> this.filesFilter.stream().anyMatch(filter -> GlobUtil.matches(filter, file.getPath())))
.map(FileInfo::getId)
.toList();
}

List<SourceString> strings = new ArrayList<>();

if (isStringsBasedProject) {
strings = client.listSourceString(null, branchId, filterLabelsStr, null, encodedCroql, null, null);
} else {
if (nonNull(fileIds) && !fileIds.isEmpty()) {
for (Long fileId : fileIds) {
strings.addAll(client.listSourceString(fileId, branchId, filterLabelsStr, null, encodedCroql, null, null));
}
} else {
strings = client.listSourceString(null, branchId, filterLabelsStr, null, encodedCroql, null, null);
}
}

strings = strings.stream()
.filter(string -> {
if (sinceFilter == null) {
return true;
}
var createdDate = string.getCreatedAt().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

return createdDate.isAfter(sinceFilter) || createdDate.isEqual(sinceFilter);
})
.toList();

strings = strings.stream()
.filter(string -> {
if (statusFilter == null) {
return true;
}

switch (statusFilter) {
case "empty" -> {
return string.getContext() == null || string.getContext().isEmpty();
}
case "ai" -> {
String aiContextSection = AiContextUtil.getAiContextSection(string.getContext());
return !aiContextSection.isEmpty();
}
case "manual" -> {
String manualContext = AiContextUtil.getManualContext(string.getContext());
return !manualContext.isEmpty();
}
default -> {
return true;
}
}
})
.toList();

if (strings.isEmpty()) {
out.println(WARNING.withIcon(RESOURCE_BUNDLE.getString("message.source_string_list_not_found")));
return;
}

out.println(OK.withIcon(String.format(RESOURCE_BUNDLE.getString("messages.context.downloaded_strings"), strings.size())));

if (outputFormat.equals("jsonl")) {
String jsonlOutput = strings.stream()
.map(string -> {
String filePath = project.getFileInfos().stream()
.filter(file -> string.getFileId() != null && file.getId().equals(string.getFileId()))
.findFirst()
.map(FileInfo::getPath)
.orElse("");

var existingRecord = existingRecords.stream()
.filter(record -> record.getId().equals(string.getId()))
.findFirst();

var stringContextRow = new AiContextUtil.StringContextRecord(
string.getId(),
string.getIdentifier(),
StringUtil.getStringText(string),
filePath,
AiContextUtil.getManualContext(string.getContext()),
existingRecord
.map(AiContextUtil.StringContextRecord::getAi_context)
.orElseGet(() -> AiContextUtil.getAiContextSection(string.getContext()))
);

return new JSONObject(stringContextRow).toString();
})
.collect(Collectors.joining("\n"));

try {
files.writeToFile(to.toString(), new ByteArrayInputStream(jsonlOutput.getBytes(UTF_8)));
out.println(OK.withIcon(String.format(RESOURCE_BUNDLE.getString("messages.context.saved_strings"), to)));
} catch (IOException e) {
throw new RuntimeException(RESOURCE_BUNDLE.getString("error.write_file"), e);
}
}
}
}
Loading
Loading