Skip to content

Commit 7e8b71f

Browse files
hive-botclaude
andcommitted
feat: add the style profile API to DeepLClient
Add listStyleProfiles(), createStyleProfile(), getStyleProfile(), updateStyleProfile() and deleteStyleProfile() for /v3/style_profiles, with the StyleProfile model and StyleProfileOptions. Options left null are not sent; an empty string or empty list is sent as given, so it clears the description or industry or unlinks all components of that kind. Style profiles are parsed tolerantly: an unparseable timestamp becomes null and non-string ID list entries are skipped. The tolerant timestamp helper moves from UsageDeserializer to Parser so both can use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 32deca6 commit 7e8b71f

12 files changed

Lines changed: 1015 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88
### Added
9+
- Added style profile support to `DeepLClient` (`/v3/style_profiles`):
10+
`createStyleProfile()`, `listStyleProfiles()`, `getStyleProfile()`,
11+
`updateStyleProfile()` and `deleteStyleProfile()`, with the new classes
12+
`StyleProfile` and `StyleProfileOptions`. Options left `null` are not sent;
13+
an empty string clears the description or industry and an empty list
14+
unlinks all components of that kind.
915
- Added Spoken Terms support to `DeepLClient` (`/v3/spoken-terms`):
1016
`createSpokenTerms()`, `listSpokenTerms()`, `getSpokenTerms()`,
1117
`updateSpokenTerms()`, `getSpokenTermsEntries()`, `replaceSpokenTermsList()`,

‎README.md‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,111 @@ class Example { // Continuing class Example from above
925925
}
926926
```
927927

928+
### Style Profiles
929+
930+
A style profile bundles glossaries, style rules and translation memories under
931+
one name, with an optional description and industry. Style profiles are managed
932+
with `DeepLClient`; each has a user-specified name and a uniquely-assigned ID.
933+
934+
#### Creating a style profile
935+
936+
Use `createStyleProfile()` to create a style profile. Only the name is required.
937+
Pass a `StyleProfileOptions` to set a description, an industry, the IDs of the
938+
components to link, and whether the glossary order sets priority. If any of the
939+
component IDs cannot be used, the request fails and no style profile is created.
940+
941+
```java
942+
class Example { // Continuing class Example from above
943+
public void createStyleProfileExample() throws Exception {
944+
// Create a style profile with only a name
945+
StyleProfile profile = client.createStyleProfile("Legal, German");
946+
System.out.println("Created: " + profile.getStyleProfileId());
947+
948+
// Create a style profile with a description and linked components
949+
StyleProfile linked = client.createStyleProfile(
950+
"Legal, German",
951+
new StyleProfileOptions()
952+
.setDescription("Contract wording for the German market.")
953+
.setIndustry("Legal")
954+
.setGlossaryIds(Arrays.asList(
955+
"4d0a2e1c-6b93-4f7a-8d15-2c6e9a3b7f81",
956+
"7c1b3d2e-0f4a-4e6b-9a8c-5d2f1e3b4a60"))
957+
.setGlossaryPriorityOrderEnabled(true)
958+
.setStyleRuleIds(Arrays.asList("dca2e053-8ae5-45e6-a0d2-881156e7f4e4")));
959+
System.out.println(linked.getStyleRuleIds());
960+
}
961+
}
962+
```
963+
964+
#### Getting and listing style profiles
965+
966+
`getStyleProfile()` retrieves a style profile by its ID or `StyleProfile` object.
967+
`listStyleProfiles()` returns one page of your style profiles. It accepts the
968+
optional parameters `page` (page number, 0-indexed, default 0) and `pageSize`
969+
(items per page, default 10; the API reduces values above 100 to 100). Optional
970+
fields that a style profile does not have, such as the description and industry,
971+
are `null`.
972+
973+
```java
974+
class Example { // Continuing class Example from above
975+
public void listStyleProfilesExample() throws Exception {
976+
StyleProfile profile = client.getStyleProfile("3f1c9d0e-7a24-4b58-9c31-8f0e5d2a1b47");
977+
System.out.println(profile.getName() + ": " + profile.getDescription());
978+
979+
for (int page = 0; ; page++) {
980+
List<StyleProfile> profiles = client.listStyleProfiles(page, 100);
981+
if (profiles.isEmpty()) {
982+
break;
983+
}
984+
for (StyleProfile p : profiles) {
985+
System.out.println(String.format("%s (%s)", p.getName(), p.getStyleProfileId()));
986+
}
987+
}
988+
}
989+
}
990+
```
991+
992+
#### Updating a style profile
993+
994+
`updateStyleProfile()` takes the ID or `StyleProfile` object, an optional new
995+
name and an optional `StyleProfileOptions`. Fields left `null` are not sent and
996+
stay unchanged. An empty string clears the description or industry. A component
997+
list you send replaces the linked list in full; an empty list unlinks all
998+
components of that kind.
999+
1000+
```java
1001+
class Example { // Continuing class Example from above
1002+
public void updateStyleProfileExample() throws Exception {
1003+
String profileId = "3f1c9d0e-7a24-4b58-9c31-8f0e5d2a1b47";
1004+
1005+
// Rename and set the industry; other fields are unchanged
1006+
client.updateStyleProfile(
1007+
profileId, "Legal, German (2026)", new StyleProfileOptions().setIndustry("Legal"));
1008+
1009+
// Clear the description and unlink all style rules
1010+
StyleProfile updated = client.updateStyleProfile(
1011+
profileId,
1012+
null,
1013+
new StyleProfileOptions()
1014+
.setDescription("")
1015+
.setStyleRuleIds(Collections.emptyList()));
1016+
System.out.println(updated.getDescription()); // null
1017+
}
1018+
}
1019+
```
1020+
1021+
#### Deleting a style profile
1022+
1023+
Use `deleteStyleProfile()` with the ID or `StyleProfile` object:
1024+
1025+
```java
1026+
class Example { // Continuing class Example from above
1027+
public void deleteStyleProfileExample() throws Exception {
1028+
client.deleteStyleProfile("3f1c9d0e-7a24-4b58-9c31-8f0e5d2a1b47");
1029+
}
1030+
}
1031+
```
1032+
9281033
### Translation Memories
9291034

9301035
Translation memories store and reuse previously created translations, helping to

‎deepl-java/src/main/java/com/deepl/api/ApiClientHelper.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ static String createQueryString(List<KeyValuePair<String, String>> queryParams)
160160
// URLEncoder encodes a space as "+", which is correct for a form body but not for a URI query
161161
// string. Any literal "+" is already escaped as %2B by URLEncoder, so every remaining "+" is a
162162
// space.
163-
private static String encodeQueryComponent(String value) throws UnsupportedEncodingException {
163+
static String encodeQueryComponent(String value) throws UnsupportedEncodingException {
164164
return URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20");
165165
}
166166
}

‎deepl-java/src/main/java/com/deepl/api/DeepLClient.java‎

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import com.deepl.api.http.HttpResponse;
88
import com.deepl.api.http.HttpResponseStream;
99
import com.deepl.api.utils.*;
10+
import com.google.gson.JsonElement;
11+
import com.google.gson.JsonObject;
1012
import java.io.*;
1113
import java.net.URLEncoder;
1214
import java.nio.charset.StandardCharsets;
@@ -1055,6 +1057,166 @@ public List<StyleRuleInfo> getAllStyleRules() throws DeepLException, Interrupted
10551057
return getAllStyleRules(null, null, null);
10561058
}
10571059

1060+
/**
1061+
* Retrieves one page of the style profiles in your account.
1062+
*
1063+
* @param page Page number to retrieve (starting from 0), or <code>null</code> for the API default
1064+
* (0).
1065+
* @param pageSize Number of items per page, or <code>null</code> for the API default (10). The
1066+
* API reduces values above 100 to 100.
1067+
* @return List of {@link StyleProfile} objects on the requested page.
1068+
* @throws InterruptedException If the thread is interrupted during execution of this function.
1069+
* @throws DeepLException If any error occurs while communicating with the DeepL API.
1070+
*/
1071+
public List<StyleProfile> listStyleProfiles(@Nullable Integer page, @Nullable Integer pageSize)
1072+
throws DeepLException, InterruptedException {
1073+
ArrayList<KeyValuePair<String, String>> queryParams = new ArrayList<>();
1074+
if (page != null) {
1075+
queryParams.add(new KeyValuePair<>("page", page.toString()));
1076+
}
1077+
if (pageSize != null) {
1078+
queryParams.add(new KeyValuePair<>("page_size", pageSize.toString()));
1079+
}
1080+
String relativeUrl = "/v3/style_profiles" + ApiClientHelper.createQueryString(queryParams);
1081+
HttpResponse response = httpClientWrapper.sendGetRequestWithBackoff(relativeUrl);
1082+
checkResponse(response, false, false);
1083+
return jsonParser.parseStyleProfileList(response.getBody());
1084+
}
1085+
1086+
/**
1087+
* Functions the same as {@link DeepLClient#listStyleProfiles(Integer, Integer)} but with default
1088+
* parameters (all null).
1089+
*
1090+
* @see DeepLClient#listStyleProfiles(Integer, Integer)
1091+
*/
1092+
public List<StyleProfile> listStyleProfiles() throws DeepLException, InterruptedException {
1093+
return listStyleProfiles(null, null);
1094+
}
1095+
1096+
/**
1097+
* Creates a style profile and returns a {@link StyleProfile} object with details about it. If any
1098+
* of the given component IDs cannot be used, the request fails and no style profile is created.
1099+
*
1100+
* @param name Name of the style profile.
1101+
* @param options Optional description, industry and linked components, or <code>null</code>.
1102+
* Fields left <code>null</code> are not sent.
1103+
* @return {@link StyleProfile} object with details about the new style profile.
1104+
* @throws InterruptedException If the thread is interrupted during execution of this function.
1105+
* @throws DeepLException If any error occurs while communicating with the DeepL API.
1106+
*/
1107+
public StyleProfile createStyleProfile(String name, @Nullable StyleProfileOptions options)
1108+
throws DeepLException, InterruptedException {
1109+
HttpResponse response =
1110+
httpClientWrapper.sendJsonRequestWithBackoff(
1111+
"/v3/style_profiles", createStyleProfileBody(name, options));
1112+
checkResponse(response, false, false);
1113+
return jsonParser.parseStyleProfile(response.getBody());
1114+
}
1115+
1116+
/**
1117+
* Functions the same as {@link DeepLClient#createStyleProfile(String, StyleProfileOptions)} but
1118+
* without options.
1119+
*
1120+
* @see DeepLClient#createStyleProfile(String, StyleProfileOptions)
1121+
*/
1122+
public StyleProfile createStyleProfile(String name) throws DeepLException, InterruptedException {
1123+
return createStyleProfile(name, null);
1124+
}
1125+
1126+
/**
1127+
* Retrieves the style profile with the given ID.
1128+
*
1129+
* @param styleProfileId ID of the style profile to retrieve.
1130+
* @return {@link StyleProfile} object with details about the style profile.
1131+
* @throws IllegalArgumentException If the ID is <code>null</code> or empty.
1132+
* @throws NotFoundException If no style profile with the given ID is found.
1133+
* @throws InterruptedException If the thread is interrupted during execution of this function.
1134+
* @throws DeepLException If any error occurs while communicating with the DeepL API.
1135+
*/
1136+
public StyleProfile getStyleProfile(String styleProfileId)
1137+
throws DeepLException, InterruptedException {
1138+
HttpResponse response =
1139+
httpClientWrapper.sendGetRequestWithBackoff(styleProfilePath(styleProfileId));
1140+
checkResponse(response, false, false);
1141+
return jsonParser.parseStyleProfile(response.getBody());
1142+
}
1143+
1144+
/**
1145+
* Functions the same as {@link DeepLClient#getStyleProfile(String)} but accepts a {@link
1146+
* StyleProfile} object.
1147+
*
1148+
* @see DeepLClient#getStyleProfile(String)
1149+
*/
1150+
public StyleProfile getStyleProfile(StyleProfile styleProfile)
1151+
throws DeepLException, InterruptedException {
1152+
return getStyleProfile(styleProfileId(styleProfile));
1153+
}
1154+
1155+
/**
1156+
* Updates the style profile with the given ID and returns the updated {@link StyleProfile}.
1157+
* Fields left <code>null</code> are not sent and stay unchanged. An empty string clears the
1158+
* description or industry; a component list that is sent replaces the linked list in full, and an
1159+
* empty list unlinks all components of that kind. With nothing set, nothing changes.
1160+
*
1161+
* @param styleProfileId ID of the style profile to update.
1162+
* @param name New name of the style profile, or <code>null</code> to keep the current name.
1163+
* @param options Fields to change, or <code>null</code>.
1164+
* @return {@link StyleProfile} object with details about the updated style profile.
1165+
* @throws IllegalArgumentException If the ID is <code>null</code> or empty.
1166+
* @throws NotFoundException If no style profile with the given ID is found.
1167+
* @throws InterruptedException If the thread is interrupted during execution of this function.
1168+
* @throws DeepLException If any error occurs while communicating with the DeepL API.
1169+
*/
1170+
public StyleProfile updateStyleProfile(
1171+
String styleProfileId, @Nullable String name, @Nullable StyleProfileOptions options)
1172+
throws DeepLException, InterruptedException {
1173+
String relativeUrl = styleProfilePath(styleProfileId);
1174+
HttpResponse response =
1175+
httpClientWrapper.sendJsonPatchRequestWithBackoff(
1176+
relativeUrl, createStyleProfileBody(name, options));
1177+
checkResponse(response, false, false);
1178+
return jsonParser.parseStyleProfile(response.getBody());
1179+
}
1180+
1181+
/**
1182+
* Functions the same as {@link DeepLClient#updateStyleProfile(String, String,
1183+
* StyleProfileOptions)} but accepts a {@link StyleProfile} object.
1184+
*
1185+
* @see DeepLClient#updateStyleProfile(String, String, StyleProfileOptions)
1186+
*/
1187+
public StyleProfile updateStyleProfile(
1188+
StyleProfile styleProfile, @Nullable String name, @Nullable StyleProfileOptions options)
1189+
throws DeepLException, InterruptedException {
1190+
return updateStyleProfile(styleProfileId(styleProfile), name, options);
1191+
}
1192+
1193+
/**
1194+
* Deletes the style profile with the given ID.
1195+
*
1196+
* @param styleProfileId ID of the style profile to delete.
1197+
* @throws IllegalArgumentException If the ID is <code>null</code> or empty.
1198+
* @throws NotFoundException If no style profile with the given ID is found.
1199+
* @throws InterruptedException If the thread is interrupted during execution of this function.
1200+
* @throws DeepLException If any error occurs while communicating with the DeepL API.
1201+
*/
1202+
public void deleteStyleProfile(String styleProfileId)
1203+
throws DeepLException, InterruptedException {
1204+
HttpResponse response =
1205+
httpClientWrapper.sendDeleteRequestWithBackoff(styleProfilePath(styleProfileId));
1206+
checkResponse(response, false, false);
1207+
}
1208+
1209+
/**
1210+
* Functions the same as {@link DeepLClient#deleteStyleProfile(String)} but accepts a {@link
1211+
* StyleProfile} object.
1212+
*
1213+
* @see DeepLClient#deleteStyleProfile(String)
1214+
*/
1215+
public void deleteStyleProfile(StyleProfile styleProfile)
1216+
throws DeepLException, InterruptedException {
1217+
deleteStyleProfile(styleProfileId(styleProfile));
1218+
}
1219+
10581220
/**
10591221
* Retrieves the languages supported by the given DeepL API resource, along with the features each
10601222
* language supports for that resource.
@@ -1713,6 +1875,40 @@ private static String spokenTermsId(SpokenTerms spokenTerms) throws IllegalArgum
17131875
return spokenTerms.getSpokenTermsId();
17141876
}
17151877

1878+
/** Extracts the ID of the given style profile. */
1879+
private static String styleProfileId(StyleProfile styleProfile) throws IllegalArgumentException {
1880+
if (styleProfile == null) {
1881+
throw new IllegalArgumentException("styleProfile must not be null");
1882+
}
1883+
return styleProfile.getStyleProfileId();
1884+
}
1885+
1886+
/** Validates the style profile ID and builds its URL path with the ID escaped. */
1887+
private String styleProfilePath(String styleProfileId) throws DeepLException {
1888+
validateParameter("styleProfileId", styleProfileId);
1889+
try {
1890+
return "/v3/style_profiles/" + ApiClientHelper.encodeQueryComponent(styleProfileId);
1891+
} catch (UnsupportedEncodingException exception) {
1892+
throw new DeepLException("Error while URL-encoding request", exception);
1893+
}
1894+
}
1895+
1896+
/** Builds the JSON body of a style profile request; Gson omits the options left null. */
1897+
private String createStyleProfileBody(
1898+
@Nullable String name, @Nullable StyleProfileOptions options) {
1899+
JsonObject body = new JsonObject();
1900+
if (name != null) {
1901+
body.addProperty("name", name);
1902+
}
1903+
if (options != null) {
1904+
for (Map.Entry<String, JsonElement> entry :
1905+
jsonParser.getGson().toJsonTree(options).getAsJsonObject().entrySet()) {
1906+
body.add(entry.getKey(), entry.getValue());
1907+
}
1908+
}
1909+
return jsonParser.getGson().toJson(body);
1910+
}
1911+
17161912
/** Builds the query string that selects the term list of the given language. */
17171913
private static String langQueryString(String lang) {
17181914
List<KeyValuePair<String, String>> queryParams = new ArrayList<>();

0 commit comments

Comments
 (0)