Compare commits

...
Author SHA1 Message Date
ConnorYohandGitHub f59d2595a6 Merge branch 'V2' into fix-analytics-v2 2025-11-26 12:28:27 +00:00
Connor Yoh 5c4be090aa Added posthog variables directly to code 2025-11-26 12:22:39 +00:00
EthanHealy01andGitHub 21657a3fa6 fix the text color for the first time setup with default credentials (#5023)
# Description of Changes

Fix for this:
<img width="584" height="203" alt="Screenshot 2025-11-26 at 11 58 17 AM"
src="https://github.com/user-attachments/assets/fb5ea4ae-e42a-407a-a890-933d9f6db829"
/>

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-26 12:14:04 +00:00
James BruntonandGitHub bc11875081 Fix CORS issues in desktop app (#5019)
# Description of Changes
In the 2.0.0 release version, the frontend can't connect to the backend
on my machine because all the network requests 403. I think this is
because of CORS issues, and supposedly these will be fixed by using a
different Spring function, which is more lenient on URL schemes (needs
to allow `tauri://localhost` here, which isn't a standard URL)
2025-11-26 10:02:38 +00:00
Reece BrowneandGitHub 31b3219169 Detect backend down (#5010)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 22:10:38 +00:00
d8a99fcb07 Use proper Windows APIs for checking/setting default app (#5000)
# Description of Changes
Use proper Windows APIs for checking/setting default app

---------

Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
2025-11-25 21:31:02 +00:00
Reece BrowneandGitHub 8016d271aa Handle non-pdf gracefully in viewer (#5004)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 21:27:47 +00:00
Anthony StirlingandGitHub 8f1bef7f46 translationsNew (#5015)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 21:25:16 +00:00
e6db57e031 grandfather users (#4984)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com>
Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
2025-11-25 21:23:32 +00:00
Reece BrowneandGitHub daf749e6be File in context indication (#4990)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 20:28:24 +00:00
Reece BrowneandGitHub 53d167eda5 Chore/v2/translation fixes (#5011)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2025-11-25 20:03:47 +00:00
d74856f675 Remove "Download for Desktop" and "Security Check" slides from desktop app onboarding (#5012)
# Description of Changes

- Title^ + I changed the text for set as default to make it more visible

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-25 19:56:09 +00:00
f8386843d4 Bug/v2/onboarding slides fix (#5005)
# Description of Changes

- Stop onboarding from appearing before logging in
- Also (should've done this in a different PR) fixed a small bug in
settings when changing logo

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
2025-11-25 18:45:40 +00:00
ConnorYohandGitHub 2277a94c91 Added default supabase and stripe public variables (#5009)
Added to codebase
VITE_SUPABASE_URL
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY
VITE_STRIPE_PUBLISHABLE_KEY
2025-11-25 17:58:38 +00:00
108 changed files with 4763 additions and 422 deletions
@@ -42,7 +42,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Tauri v1 uses tauri://localhost, v2 uses http(s)://tauri.localhost
logger.info("Tauri mode detected - enabling CORS for Tauri protocols (v1 and v2)");
registry.addMapping("/**")
.allowedOrigins(
.allowedOriginPatterns(
"tauri://localhost",
"http://tauri.localhost",
"https://tauri.localhost")
@@ -397,7 +397,8 @@ public class ProprietaryUIDataController {
Map<Long, Date> teamLastRequest = new HashMap<>();
for (Object[] result : teamActivities) {
Long teamId = (Long) result[0];
Date lastActivity = (Date) result[1];
Instant instant = (Instant) result[1];
Date lastActivity = instant != null ? Date.from(instant) : null;
teamLastRequest.put(teamId, lastActivity);
}
@@ -441,7 +442,8 @@ public class ProprietaryUIDataController {
Map<String, Date> userLastRequest = new HashMap<>();
for (Object[] result : userSessions) {
String username = (String) result[0];
Date lastRequest = (Date) result[1];
Instant instant = (Instant) result[1];
Date lastRequest = instant != null ? Date.from(instant) : null;
userLastRequest.put(username, lastRequest);
}
@@ -238,6 +238,12 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
path = "errorOAuth=oAuth2AutoCreateDisabled";
} else if (request.getParameter("oAuth2AdminBlockedUser") != null) {
path = "errorOAuth=oAuth2AdminBlockedUser";
} else if (request.getParameter("oAuth2RequiresLicense") != null) {
path = "errorOAuth=oAuth2RequiresLicense";
} else if (request.getParameter("saml2RequiresLicense") != null) {
path = "errorOAuth=saml2RequiresLicense";
} else if (request.getParameter("maxUsersReached") != null) {
path = "errorOAuth=maxUsersReached";
} else if (request.getParameter("userIsDisabled") != null) {
path = "errorOAuth=userIsDisabled";
} else if ((errorMessage = request.getParameter("error")) != null) {
@@ -62,6 +62,7 @@ public class InitialSecuritySetup {
private void initializeUserLicenseSettings() {
licenseSettingsService.initializeGrandfatheredCount();
licenseSettingsService.updateLicenseMaxUsers();
licenseSettingsService.grandfatherExistingOAuthUsers();
}
private void configureJWTSettings() {
@@ -23,7 +23,7 @@ public class PremiumEndpointAspect {
public Object checkPremiumAccess(ProceedingJoinPoint joinPoint) throws Throwable {
if (!runningProOrHigher) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN, "This endpoint requires a Pro or higher license");
HttpStatus.FORBIDDEN, "This endpoint requires a Server or Enterprise license");
}
return joinPoint.proceed();
}
@@ -84,6 +84,8 @@ public class SecurityConfiguration {
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
private final stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService;
public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository,
@@ -103,7 +105,9 @@ public class SecurityConfiguration {
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver,
stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService) {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
@@ -120,10 +124,11 @@ public class SecurityConfiguration {
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
this.licenseSettingsService = licenseSettingsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
public static PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@@ -354,7 +359,8 @@ public class SecurityConfiguration {
loginAttemptService,
securityProperties.getOauth2(),
userService,
jwtService))
jwtService,
licenseSettingsService))
.failureHandler(new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
@@ -395,7 +401,8 @@ public class SecurityConfiguration {
loginAttemptService,
securityProperties.getSaml2(),
userService,
jwtService))
jwtService,
licenseSettingsService))
.failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver(
@@ -32,7 +32,8 @@ public class EEAppConfig {
@Bean(name = "runningProOrHigher")
@Primary
public boolean runningProOrHigher() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
return license == License.SERVER || license == License.ENTERPRISE;
}
@Profile("security")
@@ -30,7 +30,7 @@ public class KeygenLicenseVerifier {
public enum License {
NORMAL,
PRO,
SERVER,
ENTERPRISE
}
@@ -76,7 +76,7 @@ public class KeygenLicenseVerifier {
log.info("Detected certificate-based license. Processing...");
boolean isValid = verifyCertificateLicense(licenseKeyOrCert, context);
if (isValid) {
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
} else {
license = License.NORMAL;
}
@@ -84,7 +84,7 @@ public class KeygenLicenseVerifier {
log.info("Detected JWT-style license key. Processing...");
boolean isValid = verifyJWTLicense(licenseKeyOrCert, context);
if (isValid) {
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
} else {
license = License.NORMAL;
}
@@ -92,7 +92,7 @@ public class KeygenLicenseVerifier {
log.info("Detected standard license key. Processing...");
boolean isValid = verifyStandardLicense(licenseKeyOrCert, context);
if (isValid) {
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
license = context.isEnterpriseLicense ? License.ENTERPRISE : License.SERVER;
} else {
license = License.NORMAL;
}
@@ -261,10 +261,23 @@ public class KeygenLicenseVerifier {
// Extract metadata
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
if (metadataObj != null) {
int users = metadataObj.optInt("users", 1);
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
// Check if this is an old license (no planType) with isEnterprise flag
context.isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
// Extract user count - default based on license type
// Old licenses: Only had isEnterprise flag
// New licenses: Have planType field with "server" or "enterprise"
int users = metadataObj.optInt("users", context.isEnterpriseLicense ? 1 : 0);
// SERVER license (isEnterprise=false, users=0) = unlimited
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
if (!context.isEnterpriseLicense && users == 0) {
log.info("SERVER license detected with unlimited users");
} else if (context.isEnterpriseLicense && users > 0) {
log.info("ENTERPRISE license allows for {} users", users);
}
applicationProperties.getPremium().setMaxUsers(users);
}
// Check license status if available
@@ -431,28 +444,31 @@ public class KeygenLicenseVerifier {
}
// Extract max users and isEnterprise from policy or metadata
int users = policyObj.optInt("users", 1);
context.isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
int users = policyObj.optInt("users", -1);
if (users > 0) {
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
} else {
// Try to get users from metadata if present
if (users == -1) {
// Try to get users from metadata if not at policy level
Object metadataObj = policyObj.opt("metadata");
if (metadataObj instanceof JSONObject metadata) {
users = metadata.optInt("users", 1);
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users (from metadata)", users);
// Check for isEnterprise flag in metadata
context.isEnterpriseLicense = metadata.optBoolean("isEnterprise", false);
context.isEnterpriseLicense =
metadata.optBoolean("isEnterprise", context.isEnterpriseLicense);
users = metadata.optInt("users", context.isEnterpriseLicense ? 1 : 0);
} else {
// Default value
applicationProperties.getPremium().setMaxUsers(1);
log.info("Using default of 1 user for license");
// Default based on license type
users = context.isEnterpriseLicense ? 1 : 0;
}
}
// SERVER license (isEnterprise=false, users=0) = unlimited
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
if (!context.isEnterpriseLicense && users == 0) {
log.info("SERVER license detected with unlimited users");
} else if (context.isEnterpriseLicense && users > 0) {
log.info("ENTERPRISE license allows for {} users", users);
}
applicationProperties.getPremium().setMaxUsers(users);
}
return true;
@@ -584,17 +600,7 @@ public class KeygenLicenseVerifier {
context.maxMachines);
}
// Extract user count, default to 1 if not specified
int users =
jsonResponse
.path("data")
.path("attributes")
.path("metadata")
.path("users")
.asInt(1);
applicationProperties.getPremium().setMaxUsers(users);
// Extract isEnterprise flag
// Extract isEnterprise flag first
context.isEnterpriseLicense =
jsonResponse
.path("data")
@@ -603,6 +609,24 @@ public class KeygenLicenseVerifier {
.path("isEnterprise")
.asBoolean(false);
// Extract user count - default based on license type
int users =
jsonResponse
.path("data")
.path("attributes")
.path("metadata")
.path("users")
.asInt(context.isEnterpriseLicense ? 1 : 0);
// SERVER license (isEnterprise=false, users=0) = unlimited
// ENTERPRISE license (isEnterprise=true, users>0) = limited seats
if (!context.isEnterpriseLicense && users == 0) {
log.info("SERVER license detected with unlimited users");
} else if (context.isEnterpriseLicense && users > 0) {
log.info("ENTERPRISE license allows for {} users", users);
}
applicationProperties.getPremium().setMaxUsers(users);
log.debug(applicationProperties.toString());
} else {
@@ -68,8 +68,8 @@ public class LicenseKeyChecker {
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
if (License.ENTERPRISE == premiumEnabledResult) {
log.info("License key is Enterprise.");
} else if (License.PRO == premiumEnabledResult) {
log.info("License key is Pro.");
} else if (License.SERVER == premiumEnabledResult) {
log.info("License key is Server.");
} else {
log.info("License key is invalid, defaulting to non pro license.");
}
@@ -87,6 +87,19 @@ public class UserController {
.body(Map.of("error", "Password is required"));
}
if (licenseSettingsService.wouldExceedLimit(1)) {
long availableSlots = licenseSettingsService.getAvailableUserSlots();
int maxAllowed = licenseSettingsService.calculateMaxAllowedUsers();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(
Map.of(
"error",
"Maximum number of users reached. Allowed: "
+ maxAllowed
+ ", Available slots: "
+ availableSlots));
}
Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
User user =
userService.saveUser(
@@ -39,4 +39,28 @@ public interface UserRepository extends JpaRepository<User, Long> {
long countByTeam(Team team);
List<User> findAllByTeam(Team team);
// OAuth grandfathering queries
long countBySsoProviderIsNotNull();
long countByOauthGrandfatheredTrue();
List<User> findAllBySsoProviderIsNotNull();
/**
* Finds all SSO users - those with sso_provider set OR authenticationType is sso/oauth2/saml2.
* This catches V1 users who were created via SSO but never signed in (sso_provider is null).
*/
@Query(
"SELECT u FROM User u WHERE u.ssoProvider IS NOT NULL "
+ "OR LOWER(u.authenticationType) IN ('sso', 'oauth2', 'saml2')")
List<User> findAllSsoUsers();
/**
* Counts all SSO users - those with sso_provider set OR authenticationType is sso/oauth2/saml2.
*/
@Query(
"SELECT COUNT(u) FROM User u WHERE u.ssoProvider IS NOT NULL "
+ "OR LOWER(u.authenticationType) IN ('sso', 'oauth2', 'saml2')")
long countSsoUsers();
}
@@ -71,6 +71,9 @@ public class User implements UserDetails, Serializable {
@Column(name = "sso_provider")
private String ssoProvider;
@Column(name = "oauth_grandfathered")
private Boolean oauthGrandfathered = false;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@@ -135,4 +138,12 @@ public class User implements UserDetails, Serializable {
public boolean hasPassword() {
return this.password != null && !this.password.isEmpty();
}
public boolean isOauthGrandfathered() {
return oauthGrandfathered != null && oauthGrandfathered;
}
public void setOauthGrandfathered(boolean oauthGrandfathered) {
this.oauthGrandfathered = oauthGrandfathered;
}
}
@@ -50,6 +50,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
private final ApplicationProperties.Security.OAUTH2 oauth2Properties;
private final UserService userService;
private final JwtServiceInterface jwtService;
private final stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService;
@Override
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
@@ -66,6 +68,25 @@ public class CustomOAuth2AuthenticationSuccessHandler
username = detailsUser.getUsername();
}
boolean userExists = userService.usernameExistsIgnoreCase(username);
// Check if user is eligible for OAuth (grandfathered or system has paid license)
if (userExists) {
stirling.software.proprietary.security.model.User user =
userService.findByUsernameIgnoreCase(username).orElse(null);
if (user != null && !licenseSettingsService.isOAuthEligible(user)) {
// User is not grandfathered and no paid license - block OAuth login
response.sendRedirect(
request.getContextPath() + "/logout?oAuth2RequiresLicense=true");
return;
}
} else if (!licenseSettingsService.isOAuthEligible(null)) {
// No existing user and no paid license -> block auto creation
response.sendRedirect(request.getContextPath() + "/logout?oAuth2RequiresLicense=true");
return;
}
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
@@ -91,7 +112,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
.sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (userService.usernameExistsIgnoreCase(username)
if (userExists
&& userService.hasPassword(username)
&& (!userService.isAuthenticationTypeByUsername(username, SSO)
|| !userService.isAuthenticationTypeByUsername(username, OAUTH2))
@@ -106,6 +127,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
response.sendRedirect(contextPath + "/logout?oAuth2AdminBlockedUser=true");
return;
}
if (!userExists && licenseSettingsService.wouldExceedLimit(1)) {
response.sendRedirect(contextPath + "/logout?maxUsersReached=true");
return;
}
if (principal instanceof OAuth2User oAuth2User) {
// Extract SSO provider information from OAuth2User
String ssoProviderId = oAuth2User.getAttribute("sub"); // OIDC ID
@@ -49,6 +49,8 @@ public class CustomSaml2AuthenticationSuccessHandler
private ApplicationProperties.Security.SAML2 saml2Properties;
private UserService userService;
private final JwtServiceInterface jwtService;
private final stirling.software.proprietary.service.UserLicenseSettingsService
licenseSettingsService;
@Override
@Audited(type = AuditEventType.USER_LOGIN, level = AuditLevel.BASIC)
@@ -63,6 +65,26 @@ public class CustomSaml2AuthenticationSuccessHandler
String username = saml2Principal.name();
log.debug("Authenticated principal found for user: {}", username);
boolean userExists = userService.usernameExistsIgnoreCase(username);
// Check if user is eligible for SAML (grandfathered or system has paid license)
if (userExists) {
stirling.software.proprietary.security.model.User user =
userService.findByUsernameIgnoreCase(username).orElse(null);
if (user != null && !licenseSettingsService.isOAuthEligible(user)) {
// User is not grandfathered and no paid license - block SAML login
response.sendRedirect(
request.getContextPath() + "/logout?saml2RequiresLicense=true");
return;
}
} else if (!licenseSettingsService.isOAuthEligible(null)) {
// No existing user and no paid license -> block auto creation
response.sendRedirect(
request.getContextPath() + "/logout?saml2RequiresLicense=true");
return;
}
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
@@ -96,7 +118,6 @@ public class CustomSaml2AuthenticationSuccessHandler
"Your account has been locked due to too many failed login attempts.");
}
boolean userExists = userService.usernameExistsIgnoreCase(username);
boolean hasPassword = userExists && userService.hasPassword(username);
boolean isSSOUser =
userExists && userService.isAuthenticationTypeByUsername(username, SSO);
@@ -129,6 +150,10 @@ public class CustomSaml2AuthenticationSuccessHandler
contextPath + "/login?errorOAuth=oAuth2AdminBlockedUser");
return;
}
if (!userExists && licenseSettingsService.wouldExceedLimit(1)) {
response.sendRedirect(contextPath + "/logout?maxUsersReached=true");
return;
}
// Extract SSO provider information from SAML2 assertion
String ssoProviderId = saml2Principal.nameId();
@@ -734,4 +734,48 @@ public class UserService implements UserServiceInterface {
public void saveAll(List<User> users) {
userRepository.saveAll(users);
}
/**
* Counts the number of OAuth/SAML users. Includes users with sso_provider set OR
* authenticationType is sso/oauth2/saml2 (catches V1 users who never signed in).
*
* @return Count of OAuth users
*/
public long countOAuthUsers() {
return userRepository.countSsoUsers();
}
/**
* Counts the number of OAuth users who are grandfathered.
*
* @return Count of grandfathered OAuth users
*/
public long countGrandfatheredOAuthUsers() {
return userRepository.countByOauthGrandfatheredTrue();
}
/**
* Grandfathers all existing OAuth/SAML users. This marks all users with an SSO provider as
* grandfathered, allowing them to keep OAuth access even without a paid license.
*
* @return Number of users updated
*/
@Transactional
public int grandfatherAllOAuthUsers() {
List<User> ssoUsers = userRepository.findAllSsoUsers();
int updated = 0;
for (User user : ssoUsers) {
if (!user.isOauthGrandfathered()) {
user.setOauthGrandfathered(true);
updated++;
}
}
if (updated > 0) {
userRepository.saveAll(ssoUsers);
}
return updated;
}
}
@@ -69,7 +69,7 @@ public class ServerCertificateService implements ServerCertificateServiceInterfa
private boolean hasProOrEnterpriseAccess() {
License license = licenseKeyChecker.getPremiumLicenseEnabledResult();
return license == License.PRO || license == License.ENTERPRISE;
return license == License.SERVER || license == License.ENTERPRISE;
}
public boolean isEnabled() {
@@ -10,6 +10,7 @@ import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -18,6 +19,8 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.model.UserLicenseSettings;
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker;
import stirling.software.proprietary.security.repository.UserLicenseSettingsRepository;
import stirling.software.proprietary.security.service.UserService;
@@ -45,6 +48,7 @@ public class UserLicenseSettingsService {
private final UserLicenseSettingsRepository settingsRepository;
private final UserService userService;
private final ApplicationProperties applicationProperties;
private final ObjectProvider<LicenseKeyChecker> licenseKeyChecker;
/**
* Gets the current user license settings, creating them if they don't exist.
@@ -151,7 +155,7 @@ public class UserLicenseSettingsService {
UserLicenseSettings settings = getOrCreateSettings();
int licenseMaxUsers = 0;
if (applicationProperties.getPremium().isEnabled()) {
if (hasPaidLicense()) {
licenseMaxUsers = applicationProperties.getPremium().getMaxUsers();
}
@@ -162,6 +166,40 @@ public class UserLicenseSettingsService {
}
}
/**
* Grandfathers existing OAuth users on first run. This is a one-time migration that marks all
* existing OAuth/SAML users as grandfathered, allowing them to keep OAuth access even without a
* paid license.
*
* <p>New users created after this migration will NOT be grandfathered and will require a paid
* license to use OAuth.
*/
@Transactional
public void grandfatherExistingOAuthUsers() {
UserLicenseSettings settings = getOrCreateSettings();
// Check if we've already run this migration
if (settings.getId() != null && settings.isGrandfatheringLocked()) {
// Migration should happen at the same time as grandfathering is locked
long oauthUsersCount = userService.countOAuthUsers();
long grandfatheredCount = userService.countGrandfatheredOAuthUsers();
if (oauthUsersCount > 0 && grandfatheredCount == 0) {
// We have OAuth users but none are grandfathered - this is first run after upgrade
int updated = userService.grandfatherAllOAuthUsers();
log.warn(
"OAuth GRANDFATHERING: Marked {} existing OAuth/SAML users as grandfathered. "
+ "They will retain OAuth access even without a paid license. "
+ "New users will require a paid license for OAuth.",
updated);
} else if (grandfatheredCount > 0) {
log.debug(
"OAuth grandfathering already completed: {} users grandfathered",
grandfatheredCount);
}
}
}
/**
* Validates and enforces the integrity of license settings. This ensures that even if someone
* manually modifies the database, the grandfathering rules are still enforced.
@@ -241,12 +279,15 @@ public class UserLicenseSettingsService {
* <p>Logic:
*
* <ul>
* <li>Grandfathered limit = max(5, existing user count at initialization)
* <li>If premium enabled: total limit = grandfathered limit + license maxUsers
* <li>If premium disabled: total limit = grandfathered limit
* <li>Grandfathered limit = max(5, existing user count at V1→V2 migration)
* <li>No license: Uses grandfathered limit only
* <li>SERVER license (maxUsers=0): Unlimited users (Integer.MAX_VALUE)
* <li>ENTERPRISE license (maxUsers>0): License seats only (NO grandfathering added)
* </ul>
*
* @return Maximum number of users allowed
* <p>IMPORTANT: Paid licenses REPLACE the limit, they don't add to grandfathering.
*
* @return Maximum number of users allowed (Integer.MAX_VALUE for unlimited)
*/
public int calculateMaxAllowedUsers() {
validateSettingsIntegrity();
@@ -259,20 +300,52 @@ public class UserLicenseSettingsService {
grandfatheredLimit = DEFAULT_USER_LIMIT;
}
int totalLimit = grandfatheredLimit;
if (applicationProperties.getPremium().isEnabled()) {
totalLimit = grandfatheredLimit + settings.getLicenseMaxUsers();
// No license: use grandfathered limit
if (!hasPaidLicense()) {
log.debug("No license: using grandfathered limit of {}", grandfatheredLimit);
return grandfatheredLimit;
}
log.debug(
"Calculated max allowed users: {} (grandfathered: {}, license: {}, premium enabled: {})",
totalLimit,
grandfatheredLimit,
settings.getLicenseMaxUsers(),
applicationProperties.getPremium().isEnabled());
int licenseMaxUsers = settings.getLicenseMaxUsers();
return totalLimit;
// SERVER license (maxUsers=0): unlimited users
if (licenseMaxUsers == 0) {
log.debug("SERVER license: unlimited users allowed");
return Integer.MAX_VALUE;
}
// ENTERPRISE license (maxUsers>0): license seats only (replaces grandfathering)
log.debug(
"ENTERPRISE license: {} seats (grandfathered {} not added)",
licenseMaxUsers,
grandfatheredLimit);
return licenseMaxUsers;
}
/**
* Checks if a user is eligible to use OAuth/SAML authentication.
*
* <p>A user is eligible if:
*
* <ul>
* <li>They are grandfathered for OAuth (existing user before policy change), OR
* <li>The system has an ENTERPRISE license (SSO is enterprise-only)
* </ul>
*
* @param user The user to check
* @return true if the user can use OAuth/SAML
*/
public boolean isOAuthEligible(stirling.software.proprietary.security.model.User user) {
// Grandfathered users always have OAuth access
if (user != null && user.isOauthGrandfathered()) {
log.debug("User {} is grandfathered for OAuth", user.getUsername());
return true;
}
// Users can use OAuth/SAML only if system has ENTERPRISE license
boolean hasEnterpriseLicense = hasEnterpriseLicense();
log.debug("OAuth eligibility check: hasEnterpriseLicense={}", hasEnterpriseLicense);
return hasEnterpriseLicense;
}
/**
@@ -408,4 +481,28 @@ public class UserLicenseSettingsService {
throw new IllegalStateException("Invalid key for grandfathered user signature", e);
}
}
private boolean hasPaidLicense() {
LicenseKeyChecker checker = licenseKeyChecker.getIfAvailable();
if (checker == null) {
return false;
}
License license = checker.getPremiumLicenseEnabledResult();
return license == License.SERVER || license == License.ENTERPRISE;
}
/**
* Checks if the system has an ENTERPRISE license. Used for enterprise-only features like SSO
* (OAuth/SAML).
*
* @return true if ENTERPRISE license is active
*/
private boolean hasEnterpriseLicense() {
LicenseKeyChecker checker = licenseKeyChecker.getIfAvailable();
if (checker == null) {
return false;
}
License license = checker.getPremiumLicenseEnabledResult();
return license == License.ENTERPRISE;
}
}
@@ -103,6 +103,9 @@ class CustomLogoutSuccessHandlerTest {
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null);
when(request.getParameter("saml2RequiresLicense")).thenReturn(null);
when(request.getParameter("maxUsersReached")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
when(request.getScheme()).thenReturn("http");
when(request.getServerName()).thenReturn("localhost");
@@ -208,6 +211,9 @@ class CustomLogoutSuccessHandlerTest {
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null);
when(request.getParameter("saml2RequiresLicense")).thenReturn(null);
when(request.getParameter("maxUsersReached")).thenReturn(null);
when(request.getParameter("userIsDisabled")).thenReturn(null);
when(request.getParameter("error")).thenReturn("!@$!@£" + error + "£$%^*$");
when(request.getScheme()).thenReturn("http");
@@ -237,6 +243,9 @@ class CustomLogoutSuccessHandlerTest {
when(request.getParameter("errorOAuth")).thenReturn(null);
when(request.getParameter("oAuth2AutoCreateDisabled")).thenReturn(null);
when(request.getParameter("oAuth2AdminBlockedUser")).thenReturn(null);
when(request.getParameter("oAuth2RequiresLicense")).thenReturn(null);
when(request.getParameter("saml2RequiresLicense")).thenReturn(null);
when(request.getParameter("maxUsersReached")).thenReturn(null);
when(request.getParameter("userIsDisabled")).thenReturn(null);
when(request.getParameter("error")).thenReturn(null);
when(request.getParameter(error)).thenReturn("true");
@@ -44,13 +44,13 @@ class LicenseKeyCheckerTest {
ApplicationProperties props = new ApplicationProperties();
props.getPremium().setEnabled(true);
props.getPremium().setKey("abc");
when(verifier.verifyLicense("abc")).thenReturn(License.PRO);
when(verifier.verifyLicense("abc")).thenReturn(License.SERVER);
LicenseKeyChecker checker =
new LicenseKeyChecker(verifier, props, userLicenseSettingsService);
checker.init();
assertEquals(License.PRO, checker.getPremiumLicenseEnabledResult());
assertEquals(License.SERVER, checker.getPremiumLicenseEnabledResult());
verify(verifier).verifyLicense("abc");
}
@@ -0,0 +1,201 @@
package stirling.software.proprietary.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.beans.factory.ObjectProvider;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.proprietary.model.UserLicenseSettings;
import stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import stirling.software.proprietary.security.configuration.ee.LicenseKeyChecker;
import stirling.software.proprietary.security.repository.UserLicenseSettingsRepository;
import stirling.software.proprietary.security.service.UserService;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class UserLicenseSettingsServiceTest {
@Mock private UserLicenseSettingsRepository settingsRepository;
@Mock private UserService userService;
@Mock private ApplicationProperties applicationProperties;
@Mock private ApplicationProperties.Premium premium;
@Mock private LicenseKeyChecker licenseKeyChecker;
@Mock private ObjectProvider<LicenseKeyChecker> licenseKeyCheckerProvider;
private UserLicenseSettingsService service;
private UserLicenseSettings mockSettings;
@BeforeEach
void setUp() {
mockSettings = new UserLicenseSettings();
mockSettings.setId(1L);
mockSettings.setGrandfatheredUserCount(80);
mockSettings.setGrandfatheringLocked(true);
mockSettings.setIntegritySalt("test-salt");
mockSettings.setGrandfatheredUserSignature("80:test-signature");
when(applicationProperties.getPremium()).thenReturn(premium);
when(settingsRepository.findSettings()).thenReturn(Optional.of(mockSettings));
when(userService.getTotalUsersCount()).thenReturn(80L);
when(settingsRepository.save(any(UserLicenseSettings.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
when(licenseKeyCheckerProvider.getIfAvailable()).thenReturn(licenseKeyChecker);
// Create service with overridden validateSettingsIntegrity to bypass signature validation
service =
new UserLicenseSettingsService(
settingsRepository,
userService,
applicationProperties,
licenseKeyCheckerProvider) {
@Override
public void validateSettingsIntegrity() {
// Override to do nothing in tests - avoid HMAC signature validation
// complexity
}
};
}
@Test
void noLicense_returnsGrandfatheredLimit() {
// No license active
when(premium.isEnabled()).thenReturn(false);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.NORMAL);
int result = service.calculateMaxAllowedUsers();
assertEquals(80, result, "Should return grandfathered user count when no license");
}
@Test
void serverLicense_returnsUnlimited() {
// SERVER license with users=0
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
mockSettings.setLicenseMaxUsers(0);
int result = service.calculateMaxAllowedUsers();
assertEquals(Integer.MAX_VALUE, result, "SERVER license should return unlimited users");
}
@Test
void enterpriseLicense_returnsLicenseSeatsOnly() {
// ENTERPRISE license with 5 seats
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(5);
int result = service.calculateMaxAllowedUsers();
assertEquals(
5,
result,
"ENTERPRISE license should return license seats only (NOT grandfathered + seats)");
}
@Test
void enterpriseLicense_ignoresGrandfathering() {
// ENTERPRISE with 20 seats, grandfathered was 80
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(20);
mockSettings.setGrandfatheredUserCount(80); // This should be ignored
int result = service.calculateMaxAllowedUsers();
assertEquals(
20,
result,
"ENTERPRISE license should ignore grandfathering and use license seats only");
}
@Test
void freshInstall_noLicense_returnsFive() {
// Fresh install with default 5 users grandfathered
mockSettings.setGrandfatheredUserCount(5);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(5, result, "Fresh install with no license should return 5 users");
}
@Test
void freshInstall_serverLicense_returnsUnlimited() {
// Fresh install with SERVER license
mockSettings.setGrandfatheredUserCount(5);
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.SERVER);
mockSettings.setLicenseMaxUsers(0);
int result = service.calculateMaxAllowedUsers();
assertEquals(
Integer.MAX_VALUE,
result,
"Fresh install with SERVER license should return unlimited");
}
@Test
void freshInstall_enterpriseLicense_returnsLicenseSeats() {
// Fresh install with ENTERPRISE 10 seats
mockSettings.setGrandfatheredUserCount(5);
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(10);
int result = service.calculateMaxAllowedUsers();
assertEquals(
10, result, "Fresh install with ENTERPRISE license should return license seats");
}
@Test
void v1MigrationWith80Users_noLicense_returns80() {
// V1V2 migration with 80 users, no paid license
mockSettings.setGrandfatheredUserCount(80);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(80, result, "V1→V2 migration should preserve 80 grandfathered users");
}
@Test
void v1MigrationWith80Users_thenEnterpriseWith5Seats_returns5() {
// V1V2 with 80 users, then buy ENTERPRISE 5 seats
mockSettings.setGrandfatheredUserCount(80);
when(premium.isEnabled()).thenReturn(true);
when(licenseKeyChecker.getPremiumLicenseEnabledResult()).thenReturn(License.ENTERPRISE);
mockSettings.setLicenseMaxUsers(5);
int result = service.calculateMaxAllowedUsers();
assertEquals(
5, result, "ENTERPRISE 5 seats should override grandfathered 80 users (not 85)");
}
@Test
void zeroGrandfathered_fallsBackToDefault() {
// Edge case: grandfathered is 0 (should not happen)
mockSettings.setGrandfatheredUserCount(0);
when(premium.isEnabled()).thenReturn(false);
int result = service.calculateMaxAllowedUsers();
assertEquals(5, result, "Should fall back to default 5 users if grandfathered is 0");
}
}
+20 -45
View File
@@ -70,7 +70,7 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@iconify-json/material-symbols": "^1.2.37",
"@iconify-json/material-symbols": "^1.2.48",
"@iconify/utils": "^3.0.2",
"@playwright/test": "^1.55.0",
"@tauri-apps/cli": "^2.5.0",
@@ -455,7 +455,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -499,7 +498,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -580,7 +578,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.4.1.tgz",
"integrity": "sha512-TGpxn2CvAKRnOJWJ3bsK+dKBiCp75ehxftRUmv7wAmPomhnG5XrDfoWJungvO+zbbqAwso6PocdeXINVt3hlAw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/engines": "1.4.1",
"@embedpdf/models": "1.4.1"
@@ -680,7 +677,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.4.1.tgz",
"integrity": "sha512-5WLDiNMH6tACkLGGv/lJtNsDeozOhSbrh0mjD1btHun8u7Yscu/Vf8tdJRUOsd+nULivo2nQ2NFNKu0OTbVo8w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -697,7 +693,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.4.1.tgz",
"integrity": "sha512-Ng02S9SFIAi9JZS5rI+NXSnZZ1Yk9YYRw4MlN2pig49qOyivZdz0oScZaYxQPewo8ccJkLeghjdeWswOBW/6cA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -715,7 +710,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.4.1.tgz",
"integrity": "sha512-m3ZOk8JygsLxoa4cZ+0BVB5pfRWuBCg2/gPqjhoFZNKTqAFw4J6HGUrhYKg94GRYe+w1cTJl/NbTBYuU5DOrsA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -752,7 +746,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.4.1.tgz",
"integrity": "sha512-gKCdNKw6WBHBEpTc2DLBWIWOxzsNnaNbpfeY6C4f2Bum0EO+XW3Hl2oIx1uaRHjIhhnXso1J3QweqelsPwDGwg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -787,7 +780,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.4.1.tgz",
"integrity": "sha512-Y9O+matB4j4fLim5s/jn7qIi+lMC9vmDJRpJhiWe8bvD9oYLP2xfD/DdhFgAjRKcNhPoxC+j8q8QN5BMeGAv2Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -824,7 +816,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.4.1.tgz",
"integrity": "sha512-lo5Ytk1PH0PrRKv6zKVupm4t02VGsqIrnSIeP6NO8Ujx0wfqEhj//sqIuO/EwfFVJD8lcQIP9UUo9y8baCrEog==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -900,7 +891,6 @@
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.4.1.tgz",
"integrity": "sha512-+TgFHKPCLTBiDYe2DdsmTS37hwQgcZ3dYIc7bE0l5cp+GVwouu1h0MTmjL+90loizeWwCiu10E/zXR6hz+CUaQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@embedpdf/models": "1.4.1"
},
@@ -1056,7 +1046,6 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1100,7 +1089,6 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -1896,9 +1884,9 @@
}
},
"node_modules/@iconify-json/material-symbols": {
"version": "1.2.44",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.44.tgz",
"integrity": "sha512-NAJjhswaK9FxBeIzFFsNygws7wHtmAkBWhF4YEwn1NZIMbA+LNITqhUiq6sP5mOdKQqnoritFTlQaZ47a5BgBg==",
"version": "1.2.48",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.48.tgz",
"integrity": "sha512-qDjqYEeNCm31tae3YbsprEklWHIoVPvqlbTuqtisNjlhHRSiXy5x+AtUq9VwjlITRU+TtqsP6jW2h+pnEUURRQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2131,7 +2119,6 @@
"resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.6.tgz",
"integrity": "sha512-paTl+0x+O/QtgMtqVJaG8maD8sfiOdgPmLOyG485FmeGZ1L3KMdEkhxZtmdGlDFsLXhmMGQ57ducT90bvhXX5A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@floating-ui/react": "^0.27.16",
"clsx": "^2.1.1",
@@ -2182,7 +2169,6 @@
"resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.6.tgz",
"integrity": "sha512-liHfaWXHAkLjJy+Bkr29UsCwAoDQ/a64WrM67lksx8F0qqyjR5RQH8zVlhuOjdpQnwtlUkE/YiTvbJiPcoI0bw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"react": "^18.x || ^19.x"
}
@@ -2250,7 +2236,6 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.5.tgz",
"integrity": "sha512-8VVxFmp1GIm9PpmnQoCoYo0UWHoOrdA57tDL62vkpzEgvb/d71Wsbv4FRg7r1Gyx7PuSo0tflH34cdl/NvfHNQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.5",
@@ -3183,7 +3168,6 @@
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz",
"integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12.16"
}
@@ -3302,6 +3286,7 @@
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz",
"integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"acorn": "^8.9.0"
}
@@ -4078,7 +4063,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4407,7 +4391,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4418,7 +4401,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4488,7 +4470,6 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3",
@@ -5202,6 +5183,7 @@
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.24.tgz",
"integrity": "sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/shared": "3.5.24"
}
@@ -5211,6 +5193,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.24.tgz",
"integrity": "sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5221,6 +5204,7 @@
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.24.tgz",
"integrity": "sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/reactivity": "3.5.24",
"@vue/runtime-core": "3.5.24",
@@ -5233,6 +5217,7 @@
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.24.tgz",
"integrity": "sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-ssr": "3.5.24",
"@vue/shared": "3.5.24"
@@ -5259,7 +5244,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5667,6 +5651,7 @@
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -5943,7 +5928,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -7000,8 +6984,7 @@
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7396,7 +7379,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7567,7 +7549,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7734,7 +7715,8 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/espree": {
"version": "10.4.0",
@@ -7799,6 +7781,7 @@
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz",
"integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
@@ -8889,7 +8872,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -9375,6 +9357,7 @@
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.6"
}
@@ -9695,7 +9678,6 @@
"integrity": "sha512-Pcfm3eZ+eO4JdZCXthW9tCDT3nF4K+9dmeZ+5X39n+Kqz0DDIABRP5CAEOHRFZk8RGuC2efksTJxrjp8EXCunQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@acemir/cssom": "^0.9.19",
"@asamuzakjp/dom-selector": "^6.7.3",
@@ -10282,7 +10264,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/locate-path": {
"version": "6.0.0",
@@ -11470,7 +11453,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -11750,7 +11732,6 @@
"resolved": "https://registry.npmjs.org/preact/-/preact-10.27.2.tgz",
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
@@ -12133,7 +12114,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -12143,7 +12123,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -13643,6 +13622,7 @@
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -13851,7 +13831,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14153,7 +14132,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -14235,7 +14213,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -14440,7 +14417,6 @@
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -14592,7 +14568,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -14606,7 +14581,6 @@
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -15218,7 +15192,8 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/zod": {
"version": "3.25.76",
+5 -5
View File
@@ -33,12 +33,12 @@
"@mantine/dates": "^8.3.1",
"@mantine/dropzone": "^8.3.1",
"@mantine/hooks": "^8.3.1",
"@stripe/react-stripe-js": "^4.0.2",
"@stripe/stripe-js": "^7.9.0",
"@supabase/supabase-js": "^2.47.13",
"@mui/icons-material": "^7.3.2",
"@mui/material": "^7.3.2",
"@reactour/tour": "^3.8.0",
"@stripe/react-stripe-js": "^4.0.2",
"@stripe/stripe-js": "^7.9.0",
"@supabase/supabase-js": "^2.47.13",
"@tailwindcss/postcss": "^4.1.13",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "^2.5.0",
@@ -57,8 +57,8 @@
"posthog-js": "^1.268.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-rnd": "^10.5.2",
"react-i18next": "^15.7.3",
"react-rnd": "^10.5.2",
"react-router-dom": "^7.9.1",
"signature_pad": "^5.0.4",
"tailwindcss": "^4.1.13",
@@ -117,7 +117,7 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@iconify-json/material-symbols": "^1.2.37",
"@iconify-json/material-symbols": "^1.2.48",
"@iconify/utils": "^3.0.2",
"@playwright/test": "^1.55.0",
"@tauri-apps/cli": "^2.5.0",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "التفضيلات"
},
"workspace": {
"title": "مساحة العمل",
"people": "الأشخاص",
"teams": "الفرق"
},
"configuration": {
"title": "التهيئة",
"systemSettings": "إعدادات النظام",
"features": "الميزات",
"endpoints": "نقاط النهاية",
"database": "قاعدة البيانات",
"advanced": "متقدم"
},
"securityAuth": {
"title": "الأمان والمصادقة",
"security": "الأمان",
"connections": "الاتصالات"
},
"licensingAnalytics": {
"title": "الترخيص والتحليلات",
"plan": "الخطة",
"audit": "التدقيق",
"usageAnalytics": "تحليلات الاستخدام"
},
"policiesPrivacy": {
"title": "السياسات والخصوصية",
"legal": "قانوني",
"privacy": "الخصوصية"
},
"developer": {
"title": "المطور",
"apiKeys": "مفاتيح API"
},
"tooltips": {
"enableLoginFirst": "فعّل وضع تسجيل الدخول أولاً",
"requiresEnterprise": "يتطلّب ترخيص Enterprise"
},
"connection": {
"title": "وضع الاتصال",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "إزالة خيارات التحويل المعطّلة في أداة التحويل بدلاً من عرضها باللون الرمادي."
},
"hotkeys": {
"title": "اختصارات لوحة المفاتيح",
"description": "خصّص اختصارات لوحة المفاتيح للوصول السريع إلى الأدوات. انقر \"تغيير الاختصار\" واضغط مجموعة مفاتيح جديدة. اضغط Esc للإلغاء.",
"errorConflict": "الاختصار مستخدم بالفعل بواسطة {{tool}}.",
"errorModifier": {
"mac": "ضمّن ⌘ (Command) أو ⌥ (Option) أو معدِّلاً آخر في اختصارك.",
"windows": "ضمّن Ctrl أو Alt أو معدِّلاً آخر في اختصارك."
},
"searchPlaceholder": "ابحث في الأدوات...",
"none": "غير معيّن",
"customBadge": "مخصص",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "اللغات المتاحة",
"description": "اللغات التي يمكن للمستخدمين الاختيار منها (اتركه فارغًا لتمكين جميع اللغات)"
"description": "اللغات التي يمكن للمستخدمين الاختيار منها (اتركه فارغًا لتمكين جميع اللغات)",
"placeholder": "اختر اللغات"
},
"customMetadata": {
"label": "بيانات وصفية مخصصة",
@@ -4061,7 +4108,9 @@
"label": "نمط الشعار",
"description": "اختر بين الشعار العصري البسيط أو أيقونة S الكلاسيكية",
"classic": "كلاسيكي",
"modern": "حديث"
"classicAlt": "شعار كلاسيكي",
"modern": "حديث",
"modernAlt": "شعار حديث"
},
"customPaths": {
"label": "مسارات مخصصة",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "تهيئة التطبيق",
"description": "إعدادات التطبيق الحالية وتفاصيل التهيئة."
"description": "إعدادات التطبيق الحالية وتفاصيل التهيئة.",
"loading": "جارٍ تحميل التهيئة...",
"error": "خطأ",
"warning": "تحذير التهيئة",
"sections": {
"basic": "تهيئة أساسية",
"security": "تهيئة الأمان",
"system": "تهيئة النظام",
"integration": "تهيئة التكامل"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "إدارة الفرق وتنظيم أعضاء مساحة العمل",
"loading": "جارٍ تحميل الفرق...",
"loadingDetails": "جارٍ تحميل تفاصيل الفريق...",
"loadError": "فشل تحميل تفاصيل الفريق",
"createNewTeam": "إنشاء فريق جديد",
"teamName": "اسم الفريق",
"totalMembers": "إجمالي الأعضاء",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "جارٍ تجهيز عملية الدفع...",
"redirecting": "جارٍ التحويل إلى الدفع الآمن...",
"upgradeTitle": "الترقية إلى {{planName}}",
"success": "تم الدفع بنجاح!",
"successMessage": "تم تفعيل اشتراكك بنجاح. ستتلقى رسالة تأكيد عبر البريد الإلكتروني قريبًا.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Seçimlər"
},
"workspace": {
"title": "İş sahəsi",
"people": "İstifadəçilər",
"teams": "Komandalar"
},
"configuration": {
"title": "Konfiqurasiya",
"systemSettings": "Sistem parametrləri",
"features": "Funksiyalar",
"endpoints": "Endpointlər",
"database": "Məlumat bazası",
"advanced": "Qabaqcıl"
},
"securityAuth": {
"title": "Təhlükəsizlik və Autentifikasiya",
"security": "Təhlükəsizlik",
"connections": "Bağlantılar"
},
"licensingAnalytics": {
"title": "Lisenziyalaşdırma və Analitika",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "İstifadə analitikası"
},
"policiesPrivacy": {
"title": "Siyasətlər və Məxfilik",
"legal": "Hüquqi",
"privacy": "Məxfilik"
},
"developer": {
"title": "Tərtibatçı",
"apiKeys": "API açarları"
},
"tooltips": {
"enableLoginFirst": "Əvvəlcə login rejimini aktiv edin",
"requiresEnterprise": "Enterprise lisenziyası tələb olunur"
},
"connection": {
"title": "Bağlantı rejimi",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Convert alətində deaktiv edilmiş çevirmə seçimlərini boz göstərmək əvəzinə siyahıdan çıxarın."
},
"hotkeys": {
"title": "Klaviatura qısayolları",
"description": "Alətlərə sürətli giriş üçün klaviatura qısayollarını fərdiləşdirin. \"Qısayolu dəyiş\" düyməsini klikləyin və yeni düymə kombinasiyasını basın. Ləğv etmək üçün Esc basın.",
"errorConflict": "Qısayol artıq {{tool}} tərəfindən istifadə olunur.",
"errorModifier": {
"mac": "Qısayolda ⌘ (Command), ⌥ (Option) və ya başqa bir modifikator daxil edin.",
"windows": "Qısayolda Ctrl, Alt və ya başqa bir modifikator daxil edin."
},
"searchPlaceholder": "Alətlərdə axtarın...",
"none": "Təyin edilməyib",
"customBadge": "Fərdi",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Mövcud Dillər",
"description": "İstifadəçilərin seçə biləcəyi dillər (hamısını aktiv etmək üçün boş saxlayın)"
"description": "İstifadəçilərin seçə biləcəyi dillər (hamısını aktiv etmək üçün boş saxlayın)",
"placeholder": "Dilləri seçin"
},
"customMetadata": {
"label": "Fərdi Metadata",
@@ -4061,7 +4108,9 @@
"label": "Logo Üslubu",
"description": "Müasir minimalist logo ilə klassik S ikonası arasında seçim edin",
"classic": "Klassik",
"modern": "Müasir"
"classicAlt": "Klassik loqo",
"modern": "Müasir",
"modernAlt": "Müasir loqo"
},
"customPaths": {
"label": "Fərdi Yollar",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Tətbiq Konfiqurasiyası",
"description": "Cari tətbiq ayarları və konfiqurasiya detalları."
"description": "Cari tətbiq ayarları və konfiqurasiya detalları.",
"loading": "Konfiqurasiya yüklənir...",
"error": "Xəta",
"warning": "Konfiqurasiya xəbərdarlığı",
"sections": {
"basic": "Əsas konfiqurasiya",
"security": "Təhlükəsizlik konfiqurasiyası",
"system": "Sistem konfiqurasiyası",
"integration": "Inteqrasiya konfiqurasiyası"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Komandaları idarə edin və iş sahəsi üzvlərini təşkil edin",
"loading": "Komandalar yüklənir...",
"loadingDetails": "Komanda detalları yüklənir...",
"loadError": "Komanda detalları yüklənmədi",
"createNewTeam": "Yeni Komanda Yaradın",
"teamName": "Komandanın adı",
"totalMembers": "Ümumi Üzv sayı",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Ödənişiniz hazırlanır...",
"redirecting": "Təhlükəsiz checkout-a yönləndirilir...",
"upgradeTitle": "{{planName}} planına yüksəldin",
"success": "Ödəniş uğurludur!",
"successMessage": "Abunəliyiniz uğurla aktivləşdirildi. Tezliklə təsdiq e-poçtu alacaqsınız.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Предпочитания"
},
"workspace": {
"title": "Работно пространство",
"people": "Хора",
"teams": "Екипи"
},
"configuration": {
"title": "Конфигурация",
"systemSettings": "Системни настройки",
"features": "Функции",
"endpoints": "Крайни точки",
"database": "База данни",
"advanced": "Разширени"
},
"securityAuth": {
"title": "Сигурност и удостоверяване",
"security": "Сигурност",
"connections": "Връзки"
},
"licensingAnalytics": {
"title": "Лицензиране и аналитика",
"plan": "План",
"audit": "Одит",
"usageAnalytics": "Аналитика на използването"
},
"policiesPrivacy": {
"title": "Политики и поверителност",
"legal": "Правни",
"privacy": "Поверителност"
},
"developer": {
"title": "Разработчик",
"apiKeys": "API ключове"
},
"tooltips": {
"enableLoginFirst": "Първо активирайте режим за вход",
"requiresEnterprise": "Изисква Enterprise лиценз"
},
"connection": {
"title": "Режим на свързване",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Премахва деактивираните опции за конвертиране в инструмента Convert, вместо да ги показва посивени."
},
"hotkeys": {
"title": "Клавишни комбинации",
"description": "Персонализирайте клавишните комбинации за бърз достъп до инструментите. Щракнете \"Промяна на комбинацията\" и натиснете нова клавишна комбинация. Натиснете Esc за отказ.",
"errorConflict": "Комбинацията вече се използва от {{tool}}.",
"errorModifier": {
"mac": "Включете ⌘ (Command), ⌥ (Option) или друг модификатор в комбинацията.",
"windows": "Включете Ctrl, Alt или друг модификатор в комбинацията."
},
"searchPlaceholder": "Търсене на инструменти...",
"none": "Не е зададено",
"customBadge": "Потребителски",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Налични езици",
"description": "Езици, от които потребителите могат да избират (оставете празно, за да активирате всички езици)"
"description": "Езици, от които потребителите могат да избират (оставете празно, за да активирате всички езици)",
"placeholder": "Изберете езици"
},
"customMetadata": {
"label": "Персонализирани метаданни",
@@ -4061,7 +4108,9 @@
"label": "Стил на логото",
"description": "Изберете между модерното минималистично лого и класическата S икона",
"classic": "Класическо",
"modern": "Модерно"
"classicAlt": "Класическо лого",
"modern": "Модерно",
"modernAlt": "Модерно лого"
},
"customPaths": {
"label": "Персонализирани пътища",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Конфигурация на приложението",
"description": "Текущи настройки и подробности за конфигурацията на приложението."
"description": "Текущи настройки и подробности за конфигурацията на приложението.",
"loading": "Зареждане на конфигурацията...",
"error": "Грешка",
"warning": "Предупреждение за конфигурация",
"sections": {
"basic": "Базова конфигурация",
"security": "Конфигурация за сигурност",
"system": "Системна конфигурация",
"integration": "Конфигурация за интеграции"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Управлявайте екипи и организирайте членовете на работното пространство",
"loading": "Зареждане на екипи...",
"loadingDetails": "Зареждане на детайли за екип...",
"loadError": "Неуспешно зареждане на данните за екипа",
"createNewTeam": "Създаване на нов екип",
"teamName": "Име на екипа",
"totalMembers": "Общ брой членове",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Подготвя се плащането ви...",
"redirecting": "Пренасочване към защитено плащане...",
"upgradeTitle": "Надградете до {{planName}}",
"success": "Плащането е успешно!",
"successMessage": "Абонаментът ви беше активиран успешно. Скоро ще получите потвърждение по имейл.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferències"
},
"workspace": {
"title": "Espai de treball",
"people": "Persones",
"teams": "Equips"
},
"configuration": {
"title": "Configuració",
"systemSettings": "Configuració del sistema",
"features": "Funcions",
"endpoints": "Endpoints",
"database": "Base de dades",
"advanced": "Avançat"
},
"securityAuth": {
"title": "Seguretat i autenticació",
"security": "Seguretat",
"connections": "Connexions"
},
"licensingAnalytics": {
"title": "Llicències i analítiques",
"plan": "Pla",
"audit": "Auditoria",
"usageAnalytics": "Analítiques d'ús"
},
"policiesPrivacy": {
"title": "Polítiques i privadesa",
"legal": "Legal",
"privacy": "Privadesa"
},
"developer": {
"title": "Desenvolupador",
"apiKeys": "Claus API"
},
"tooltips": {
"enableLoginFirst": "Activa primer el mode d'inici de sessió",
"requiresEnterprise": "Requereix llicència Enterprise"
},
"connection": {
"title": "Mode de connexió",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Elimina les opcions de conversió desactivades a l'eina Convertir en lloc de mostrar-les en gris."
},
"hotkeys": {
"title": "Dreceres de teclat",
"description": "Personalitza les dreceres de teclat per accedir ràpidament a les eines. Fes clic a \"Canvia la drecera\" i prem una nova combinació de tecles. Prem Esc per cancel·lar.",
"errorConflict": "Drecera ja utilitzada per {{tool}}.",
"errorModifier": {
"mac": "Inclou ⌘ (Command), ⌥ (Option) o un altre modificador a la drecera.",
"windows": "Inclou Ctrl, Alt o un altre modificador a la drecera."
},
"searchPlaceholder": "Cerca eines...",
"none": "Sense assignar",
"customBadge": "Personalitzada",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Idiomes disponibles",
"description": "Idiomes que els usuaris poden seleccionar (deixeu-ho buit per habilitar tots els idiomes)"
"description": "Idiomes que els usuaris poden seleccionar (deixeu-ho buit per habilitar tots els idiomes)",
"placeholder": "Selecciona idiomes"
},
"customMetadata": {
"label": "Metadades personalitzades",
@@ -4061,7 +4108,9 @@
"label": "Estil del logotip",
"description": "Trieu entre el logotip modern minimalista o la icona clàssica S",
"classic": "Clàssic",
"modern": "Modern"
"classicAlt": "Logotip clàssic",
"modern": "Modern",
"modernAlt": "Logotip modern"
},
"customPaths": {
"label": "Camins personalitzats",
@@ -4499,7 +4548,7 @@
"description": "URL o nom de fitxer de la política de galetes"
},
"impressum": {
"label": "Impressum",
"label": "Avís legal",
"description": "URL o nom de fitxer de l'impressum (requerit en algunes jurisdiccions)"
}
},
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configuració de laplicació",
"description": "Paràmetres actuals de laplicació i detalls de configuració."
"description": "Paràmetres actuals de laplicació i detalls de configuració.",
"loading": "Carregant la configuració...",
"error": "Error",
"warning": "Avís de configuració",
"sections": {
"basic": "Configuració bàsica",
"security": "Configuració de seguretat",
"system": "Configuració del sistema",
"integration": "Configuració d'integració"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gestioneu equips i organitzeu els membres de lespai de treball",
"loading": "Carregant equips...",
"loadingDetails": "Carregant detalls de lequip...",
"loadError": "No s'han pogut carregar els detalls de l'equip",
"createNewTeam": "Crea un equip nou",
"teamName": "Nom de lequip",
"totalMembers": "Membres totals",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Sestà preparant el pagament...",
"redirecting": "Redirigint al pagament segur...",
"upgradeTitle": "Actualitza a {{planName}}",
"success": "Pagament correcte!",
"successMessage": "La vostra subscripció sha activat correctament. Rebreu un correu de confirmació en breu.",
+67 -7
View File
@@ -317,11 +317,51 @@
}
},
"settings": {
"preferences": {
"title": "Předvolby"
},
"workspace": {
"title": "Pracovní prostor",
"people": "Lidé",
"teams": "Týmy"
},
"configuration": {
"title": "Konfigurace",
"systemSettings": "Systémová nastavení",
"features": "Funkce",
"endpoints": "Endpoints",
"database": "Databáze",
"advanced": "Pokročilé"
},
"securityAuth": {
"title": "Zabezpečení a ověřování",
"security": "Zabezpečení",
"connections": "Připojení"
},
"licensingAnalytics": {
"title": "Licencování a analytika",
"plan": "Plán",
"audit": "Audit",
"usageAnalytics": "Analytika využití"
},
"policiesPrivacy": {
"title": "Zásady a soukromí",
"legal": "Právní",
"privacy": "Soukromí"
},
"developer": {
"title": "Vývojář",
"apiKeys": "API klíče"
},
"tooltips": {
"enableLoginFirst": "Nejprve povolte režim přihlášení",
"requiresEnterprise": "Vyžaduje licenci Enterprise"
},
"connection": {
"title": "Režim připojení",
"mode": {
"saas": "Stirling Cloud",
"selfhosted": "Self-Hosted"
"selfhosted": "Vlastní hosting"
},
"server": "Server",
"user": "Přihlášen jako",
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Místo šedých voleb odeberte zakázané možnosti převodu v nástroji Převod."
},
"hotkeys": {
"title": "Klávesové zkratky",
"description": "Přizpůsobte si klávesové zkratky pro rychlý přístup k nástrojům. Klikněte na „Změnit zkratku“ a stiskněte novou kombinaci kláves. Stisknutím Esc zrušíte.",
"errorConflict": "Zkratku již používá {{tool}}.",
"errorModifier": {
"mac": "Do zkratky zahrňte ⌘ (Command), ⌥ (Option) nebo jiný modifikátor.",
"windows": "Do zkratky zahrňte Ctrl, Alt nebo jiný modifikátor."
},
"searchPlaceholder": "Hledat nástroje...",
"none": "Nepřiřazeno",
"customBadge": "Vlastní",
@@ -1165,9 +1211,9 @@
"fileFormat": "Formát souboru",
"wordDoc": "Dokument Word",
"wordDocExt": "Dokument Word (.docx)",
"odtExt": "OpenDocument Text (.odt)",
"odtExt": "Text OpenDocument (.odt)",
"pptExt": "PowerPoint (.pptx)",
"odpExt": "OpenDocument Presentation (.odp)",
"odpExt": "Prezentace OpenDocument (.odp)",
"txtExt": "Prostý text (.txt)",
"rtfExt": "Rich Text Format (.rtf)",
"selectedFiles": "Vybrané soubory",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Dostupné jazyky",
"description": "Jazyky, které si mohou uživatelé vybrat (ponechte prázdné pro povolení všech jazyků)"
"description": "Jazyky, které si mohou uživatelé vybrat (ponechte prázdné pro povolení všech jazyků)",
"placeholder": "Vyberte jazyky"
},
"customMetadata": {
"label": "Vlastní metadata",
@@ -4061,7 +4108,9 @@
"label": "Styl loga",
"description": "Vyberte mezi moderním minimalistickým logem a klasickou ikonou S",
"classic": "Klasický",
"modern": "Moderní"
"classicAlt": "Klasické logo",
"modern": "Moderní",
"modernAlt": "Moderní logo"
},
"customPaths": {
"label": "Vlastní cesty",
@@ -4504,7 +4553,7 @@
}
},
"premium": {
"title": "Premium & Enterprise",
"title": "Premium a Enterprise",
"description": "Nakonfigurujte svůj prémiový nebo enterprise licenční klíč.",
"license": "Konfigurace licence",
"licenseKey": {
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfigurace aplikace",
"description": "Aktuální nastavení aplikace a detaily konfigurace."
"description": "Aktuální nastavení aplikace a detaily konfigurace.",
"loading": "Načítání konfigurace...",
"error": "Chyba",
"warning": "Konfigurační varování",
"sections": {
"basic": "Základní konfigurace",
"security": "Bezpečnostní konfigurace",
"system": "Systémová konfigurace",
"integration": "Integrační konfigurace"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Spravujte týmy a organizujte členy pracovního prostoru",
"loading": "Načítání týmů...",
"loadingDetails": "Načítání detailů týmu...",
"loadError": "Nepodařilo se načíst detaily týmu",
"createNewTeam": "Vytvořit nový tým",
"teamName": "Název týmu",
"totalMembers": "Celkem členů",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Připravujeme vaši platbu...",
"redirecting": "Přesměrování na zabezpečenou pokladnu...",
"upgradeTitle": "Upgradovat na {{planName}}",
"success": "Platba úspěšná!",
"successMessage": "Vaše předplatné bylo úspěšně aktivováno. Brzy obdržíte potvrzující email.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Præferencer"
},
"workspace": {
"title": "Arbejdsområde",
"people": "Personer",
"teams": "Teams"
},
"configuration": {
"title": "Konfiguration",
"systemSettings": "Systemindstillinger",
"features": "Funktioner",
"endpoints": "Endpoints",
"database": "Database",
"advanced": "Avanceret"
},
"securityAuth": {
"title": "Sikkerhed og autentificering",
"security": "Sikkerhed",
"connections": "Forbindelser"
},
"licensingAnalytics": {
"title": "Licensering & Analytics",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "Brugsanalyse"
},
"policiesPrivacy": {
"title": "Politikker & Privatliv",
"legal": "Juridisk",
"privacy": "Privatliv"
},
"developer": {
"title": "Udvikler",
"apiKeys": "API-nøgler"
},
"tooltips": {
"enableLoginFirst": "Aktivér login-tilstand først",
"requiresEnterprise": "Kræver Enterprise-licens"
},
"connection": {
"title": "Forbindelsestilstand",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Fjern deaktiverede konverteringsmuligheder i Konverter-værktøjet i stedet for at vise dem nedtonet."
},
"hotkeys": {
"title": "Tastaturgenveje",
"description": "Tilpas tastaturgenveje for hurtig adgang til værktøjer. Klik på \"Skift genvej\" og tryk en ny tastekombination. Tryk Esc for at annullere.",
"errorConflict": "Genvej bruges allerede af {{tool}}.",
"errorModifier": {
"mac": "Medtag ⌘ (Command), ⌥ (Option) eller en anden modifikatortast i din genvej.",
"windows": "Medtag Ctrl, Alt eller en anden modifikatortast i din genvej."
},
"searchPlaceholder": "Søg værktøjer...",
"none": "Ikke tildelt",
"customBadge": "Brugerdefineret",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Tilgængelige sprog",
"description": "Sprog som brugere kan vælge imellem (lad være tomt for at aktivere alle sprog)"
"description": "Sprog som brugere kan vælge imellem (lad være tomt for at aktivere alle sprog)",
"placeholder": "Vælg sprog"
},
"customMetadata": {
"label": "Brugerdefinerede metadata",
@@ -4061,7 +4108,9 @@
"label": "Logostil",
"description": "Vælg mellem det moderne minimalistiske logo eller det klassiske S-ikon",
"classic": "Klassisk",
"modern": "Moderne"
"classicAlt": "Klassisk logo",
"modern": "Moderne",
"modernAlt": "Moderne logo"
},
"customPaths": {
"label": "Brugerdefinerede stier",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Applikationskonfiguration",
"description": "Aktuelle indstillinger og konfigurationsdetaljer for applikationen."
"description": "Aktuelle indstillinger og konfigurationsdetaljer for applikationen.",
"loading": "Indlæser konfiguration...",
"error": "Fejl",
"warning": "Konfigurationsadvarsel",
"sections": {
"basic": "Basiskonfiguration",
"security": "Sikkerhedskonfiguration",
"system": "Systemkonfiguration",
"integration": "Integrationskonfiguration"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Administrér teams og organisér medlemmer i arbejdsområdet",
"loading": "Indlæser teams...",
"loadingDetails": "Indlæser teamdetaljer...",
"loadError": "Kunne ikke indlæse teamdetaljer",
"createNewTeam": "Opret nyt team",
"teamName": "Teamnavn",
"totalMembers": "Samlet antal medlemmer",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Forbereder din betaling...",
"redirecting": "Viderestiller til sikker checkout...",
"upgradeTitle": "Opgrader til {{planName}}",
"success": "Betaling gennemført!",
"successMessage": "Dit abonnement er blevet aktiveret. Du modtager snart en bekræftelsesmail.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Einstellungen"
},
"workspace": {
"title": "Arbeitsbereich",
"people": "Personen",
"teams": "Teams"
},
"configuration": {
"title": "Konfiguration",
"systemSettings": "Systemeinstellungen",
"features": "Funktionen",
"endpoints": "Endpoints",
"database": "Datenbank",
"advanced": "Erweitert"
},
"securityAuth": {
"title": "Sicherheit & Authentifizierung",
"security": "Sicherheit",
"connections": "Verbindungen"
},
"licensingAnalytics": {
"title": "Lizenzierung & Analytics",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "Nutzungsanalyse"
},
"policiesPrivacy": {
"title": "Richtlinien & Datenschutz",
"legal": "Rechtliches",
"privacy": "Datenschutz"
},
"developer": {
"title": "Entwickler",
"apiKeys": "API-Schlüssel"
},
"tooltips": {
"enableLoginFirst": "Login-Modus zuerst aktivieren",
"requiresEnterprise": "Erfordert Enterprise-Lizenz"
},
"connection": {
"title": "Verbindungsmodus",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Deaktivierte Konvertierungsoptionen im Konvertieren-Tool ausblenden statt ausgegraut anzuzeigen."
},
"hotkeys": {
"title": "Tastenkürzel",
"description": "Tastenkürzel für schnellen Tool-Zugriff anpassen. Klicken Sie auf \"Shortcut ändern\" und drücken Sie eine neue Tastenkombination. Drücken Sie Esc, um abzubrechen.",
"errorConflict": "Kürzel wird bereits von {{tool}} verwendet.",
"errorModifier": {
"mac": "Fügen Sie ⌘ (Command), ⌥ (Option) oder einen anderen Modifikator in Ihr Kürzel ein.",
"windows": "Fügen Sie Strg, Alt oder einen anderen Modifikator in Ihr Kürzel ein."
},
"searchPlaceholder": "Tools durchsuchen...",
"none": "Nicht zugewiesen",
"customBadge": "Benutzerdefiniert",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Verfügbare Sprachen",
"description": "Sprachen, aus denen Benutzer auswählen können (leer lassen, um alle Sprachen zu aktivieren)"
"description": "Sprachen, aus denen Benutzer auswählen können (leer lassen, um alle Sprachen zu aktivieren)",
"placeholder": "Sprachen auswählen"
},
"customMetadata": {
"label": "Benutzerdefinierte Metadaten",
@@ -4061,7 +4108,9 @@
"label": "Logo-Stil",
"description": "Zwischen modernem, minimalistischem Logo und klassischem S-Symbol wählen",
"classic": "Klassisch",
"modern": "Modern"
"classicAlt": "Klassisches Logo",
"modern": "Modern",
"modernAlt": "Modernes Logo"
},
"customPaths": {
"label": "Benutzerdefinierte Pfade",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Anwendungskonfiguration",
"description": "Aktuelle Anwendungseinstellungen und Konfigurationsdetails."
"description": "Aktuelle Anwendungseinstellungen und Konfigurationsdetails.",
"loading": "Konfiguration wird geladen...",
"error": "Fehler",
"warning": "Konfigurationswarnung",
"sections": {
"basic": "Grundkonfiguration",
"security": "Sicherheitskonfiguration",
"system": "Systemkonfiguration",
"integration": "Integrationskonfiguration"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Teams verwalten und Mitglieder des Arbeitsbereichs organisieren",
"loading": "Teams werden geladen...",
"loadingDetails": "Teamdetails werden geladen...",
"loadError": "Teamdetails konnten nicht geladen werden",
"createNewTeam": "Neues Team erstellen",
"teamName": "Teamname",
"totalMembers": "Mitglieder insgesamt",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Checkout wird vorbereitet...",
"redirecting": "Weiterleitung zum sicheren Checkout...",
"upgradeTitle": "Upgrade auf {{planName}}",
"success": "Zahlung erfolgreich!",
"successMessage": "Ihr Abonnement wurde erfolgreich aktiviert. Sie erhalten in Kürze eine Bestätigungs-E-Mail.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Προτιμήσεις"
},
"workspace": {
"title": "Χώρος εργασίας",
"people": "Άτομα",
"teams": "Ομάδες"
},
"configuration": {
"title": "Διαμόρφωση",
"systemSettings": "Ρυθμίσεις συστήματος",
"features": "Δυνατότητες",
"endpoints": "Endpoints",
"database": "Βάση δεδομένων",
"advanced": "Προχωρημένα"
},
"securityAuth": {
"title": "Ασφάλεια & Αυθεντικοποίηση",
"security": "Ασφάλεια",
"connections": "Συνδέσεις"
},
"licensingAnalytics": {
"title": "Άδειες & Αναλύσεις",
"plan": "Πλάνο",
"audit": "Έλεγχος",
"usageAnalytics": "Αναλύσεις χρήσης"
},
"policiesPrivacy": {
"title": "Πολιτικές & Απόρρητο",
"legal": "Νομικά",
"privacy": "Απόρρητο"
},
"developer": {
"title": "Προγραμματιστής",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "Ενεργοποιήστε πρώτα τη λειτουργία σύνδεσης",
"requiresEnterprise": "Απαιτεί άδεια Enterprise"
},
"connection": {
"title": "Λειτουργία σύνδεσης",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Κατάργηση απενεργοποιημένων επιλογών μετατροπής στο εργαλείο Convert αντί να εμφανίζονται ξεθωριασμένες."
},
"hotkeys": {
"title": "Συντομεύσεις πληκτρολογίου",
"description": "Προσαρμόστε τις συντομεύσεις για γρήγορη πρόσβαση στα εργαλεία. Κάντε κλικ στο \"Αλλαγή συντόμευσης\" και πατήστε νέο συνδυασμό. Πατήστε Esc για ακύρωση.",
"errorConflict": "Η συντόμευση χρησιμοποιείται ήδη από το {{tool}}.",
"errorModifier": {
"mac": "Συμπεριλάβετε το ⌘ (Command), ⌥ (Option), ή άλλο τροποποιητή στη συντόμευσή σας.",
"windows": "Συμπεριλάβετε το Ctrl, Alt, ή άλλο τροποποιητή στη συντόμευσή σας."
},
"searchPlaceholder": "Αναζήτηση εργαλείων...",
"none": "Μη εκχωρημένο",
"customBadge": "Προσαρμοσμένη",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Διαθέσιμες γλώσσες",
"description": "Γλώσσες από τις οποίες μπορούν να επιλέξουν οι χρήστες (αφήστε κενό για ενεργοποίηση όλων)"
"description": "Γλώσσες από τις οποίες μπορούν να επιλέξουν οι χρήστες (αφήστε κενό για ενεργοποίηση όλων)",
"placeholder": "Επιλέξτε γλώσσες"
},
"customMetadata": {
"label": "Προσαρμοσμένα μεταδεδομένα",
@@ -4061,7 +4108,9 @@
"label": "Στυλ λογοτύπου",
"description": "Επιλέξτε μεταξύ του σύγχρονου μινιμαλιστικού λογότυπου ή του κλασικού εικονιδίου S",
"classic": "Κλασικό",
"modern": "Σύγχρονο"
"classicAlt": "Κλασικό λογότυπο",
"modern": "Σύγχρονο",
"modernAlt": "Σύγχρονο λογότυπο"
},
"customPaths": {
"label": "Προσαρμοσμένες διαδρομές",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Ρυθμίσεις εφαρμογής",
"description": "Τρέχουσες ρυθμίσεις εφαρμογής και λεπτομέρειες διαμόρφωσης."
"description": "Τρέχουσες ρυθμίσεις εφαρμογής και λεπτομέρειες διαμόρφωσης.",
"loading": "Φόρτωση ρυθμίσεων...",
"error": "Σφάλμα",
"warning": "Προειδοποίηση ρυθμίσεων",
"sections": {
"basic": "Βασικές ρυθμίσεις",
"security": "Ρυθμίσεις ασφάλειας",
"system": "Ρυθμίσεις συστήματος",
"integration": "Ρυθμίσεις ενσωμάτωσης"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Διαχείριση ομάδων και οργάνωση μελών του χώρου εργασίας",
"loading": "Φόρτωση ομάδων...",
"loadingDetails": "Φόρτωση λεπτομερειών ομάδας...",
"loadError": "Αποτυχία φόρτωσης στοιχείων ομάδας",
"createNewTeam": "Δημιουργία νέας ομάδας",
"teamName": "Όνομα ομάδας",
"totalMembers": "Σύνολο μελών",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Προετοιμασία ολοκλήρωσης αγοράς...",
"redirecting": "Γίνεται ανακατεύθυνση στο ασφαλές checkout...",
"upgradeTitle": "Αναβάθμιση σε {{planName}}",
"success": "Επιτυχής πληρωμή!",
"successMessage": "Η συνδρομή σας ενεργοποιήθηκε με επιτυχία. Θα λάβετε σύντομα email επιβεβαίωσης.",
+80 -8
View File
@@ -40,6 +40,11 @@
"discardChanges": "Discard & Leave",
"applyAndContinue": "Save & Leave",
"exportAndContinue": "Export & Continue",
"backendStartup": {
"notFoundTitle": "Backend not found",
"retry": "Retry",
"unreachable": "The application cannot currently connect to the backend. Verify the backend status and network connectivity, then try again."
},
"zipWarning": {
"title": "Large ZIP File",
"message": "This ZIP contains {{count}} files. Extract anyway?",
@@ -63,7 +68,7 @@
},
"settingsOpened": {
"title": "Settings Opened",
"message": "Please select Stirling PDF in your system settings"
"message": "In Windows Settings, search for 'PDF' and select Stirling PDF as your default app"
},
"error": {
"title": "Error",
@@ -3473,6 +3478,9 @@
"ssoSignIn": "Login via Single Sign-on",
"oAuth2AutoCreateDisabled": "OAUTH2 Auto-Create User Disabled",
"oAuth2AdminBlockedUser": "Registration or logging in of non-registered users is currently blocked. Please contact the administrator.",
"oAuth2RequiresLicense": "OAuth/SSO login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan.",
"saml2RequiresLicense": "SAML login requires a paid license (Server or Enterprise). Please contact the administrator to upgrade your plan.",
"maxUsersReached": "Maximum number of users reached for your current license. Please contact the administrator to upgrade your plan or add more seats.",
"oauth2RequestNotFound": "Authorization request not found",
"oauth2InvalidUserInfoResponse": "Invalid User Info Response",
"oauth2invalidRequest": "Invalid Request",
@@ -3887,14 +3895,17 @@
"actualSize": "Actual Size"
},
"viewer": {
"cannotPreviewFile": "Cannot Preview File",
"dualPageView": "Dual Page View",
"firstPage": "First Page",
"lastPage": "Last Page",
"previousPage": "Previous Page",
"nextPage": "Next Page",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out",
"onlyPdfSupported": "The viewer only supports PDF files. This file appears to be a different format.",
"previousPage": "Previous Page",
"singlePageView": "Single Page View",
"dualPageView": "Dual Page View"
"unknownFile": "Unknown file",
"zoomIn": "Zoom In",
"zoomOut": "Zoom Out"
},
"rightRail": {
"closeSelected": "Close Selected Files",
@@ -4059,7 +4070,8 @@
},
"languages": {
"label": "Available Languages",
"description": "Languages that users can select from (leave empty to enable all languages)"
"description": "Languages that users can select from (leave empty to enable all languages)",
"placeholder": "Select languages"
},
"customMetadata": {
"label": "Custom Metadata",
@@ -4084,7 +4096,9 @@
"label": "Logo Style",
"description": "Set the default logo style for all users on this server. Users can override this setting in their personal preferences.",
"classic": "Classic",
"modern": "Modern"
"classicAlt": "Classic logo",
"modern": "Modern",
"modernAlt": "Modern logo"
},
"customPaths": {
"label": "Custom Paths",
@@ -4683,7 +4697,9 @@
"download": "Download",
"delete": "Delete",
"unsupported": "Unsupported",
"active": "Active",
"addToUpload": "Add to Upload",
"closeFile": "Close File",
"deleteAll": "Delete All",
"loadingFiles": "Loading files...",
"noFiles": "No files available",
@@ -4992,7 +5008,16 @@
"config": {
"overview": {
"title": "Application Configuration",
"description": "Current application settings and configuration details."
"description": "Current application settings and configuration details.",
"loading": "Loading configuration...",
"error": "Error",
"warning": "Configuration Warning",
"sections": {
"basic": "Basic Configuration",
"security": "Security Configuration",
"system": "System Configuration",
"integration": "Integration Configuration"
}
},
"account": {
"overview": {
@@ -5367,6 +5392,7 @@
"description": "Manage teams and organize workspace members",
"loading": "Loading teams...",
"loadingDetails": "Loading team details...",
"loadError": "Failed to load team details",
"createNewTeam": "Create New Team",
"teamName": "Team Name",
"totalMembers": "Total Members",
@@ -5896,6 +5922,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferences"
},
"workspace": {
"title": "Workspace",
"people": "People",
"teams": "Teams"
},
"configuration": {
"title": "Configuration",
"systemSettings": "System Settings",
"features": "Features",
"endpoints": "Endpoints",
"database": "Database",
"advanced": "Advanced"
},
"securityAuth": {
"title": "Security & Authentication",
"security": "Security",
"connections": "Connections"
},
"licensingAnalytics": {
"title": "Licensing & Analytics",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "Usage Analytics"
},
"policiesPrivacy": {
"title": "Policies & Privacy",
"legal": "Legal",
"privacy": "Privacy"
},
"developer": {
"title": "Developer",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "Enable login mode first",
"requiresEnterprise": "Requires Enterprise license"
},
"connection": {
"title": "Connection Mode",
"mode": {
@@ -5952,7 +6018,13 @@
"hideUnavailableConversionsDescription": "Remove disabled conversion options in the Convert tool instead of showing them greyed out."
},
"hotkeys": {
"title": "Keyboard Shortcuts",
"description": "Customize keyboard shortcuts for quick tool access. Click \"Change shortcut\" and press a new key combination. Press Esc to cancel.",
"errorConflict": "Shortcut already used by {{tool}}.",
"errorModifier": {
"mac": "Include ⌘ (Command), ⌥ (Option), or another modifier in your shortcut.",
"windows": "Include Ctrl, Alt, or another modifier in your shortcut."
},
"searchPlaceholder": "Search tools...",
"none": "Not assigned",
"customBadge": "Custom",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferencias"
},
"workspace": {
"title": "Espacio de trabajo",
"people": "Personas",
"teams": "Equipos"
},
"configuration": {
"title": "Configuración",
"systemSettings": "Ajustes del sistema",
"features": "Funciones",
"endpoints": "Endpoints",
"database": "Base de datos",
"advanced": "Avanzado"
},
"securityAuth": {
"title": "Seguridad y autenticación",
"security": "Seguridad",
"connections": "Conexiones"
},
"licensingAnalytics": {
"title": "Licencias y analítica",
"plan": "Plan",
"audit": "Auditoría",
"usageAnalytics": "Analítica de uso"
},
"policiesPrivacy": {
"title": "Políticas y privacidad",
"legal": "Legal",
"privacy": "Privacidad"
},
"developer": {
"title": "Desarrollador",
"apiKeys": "Claves API"
},
"tooltips": {
"enableLoginFirst": "Activa primero el modo de inicio de sesión",
"requiresEnterprise": "Requiere licencia Enterprise"
},
"connection": {
"title": "Modo de conexión",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Eliminar las opciones de conversión desactivadas en la herramienta Convertir en lugar de mostrarlas en gris."
},
"hotkeys": {
"title": "Atajos de teclado",
"description": "Personaliza los atajos de teclado para acceder rápido a las herramientas. Haz clic en \"Cambiar atajo\" y pulsa una nueva combinación de teclas. Pulsa Esc para cancelar.",
"errorConflict": "Atajo ya utilizado por {{tool}}.",
"errorModifier": {
"mac": "Incluye ⌘ (Command), ⌥ (Option) u otro modificador en tu atajo.",
"windows": "Incluye Ctrl, Alt u otro modificador en tu atajo."
},
"searchPlaceholder": "Buscar herramientas...",
"none": "No asignado",
"customBadge": "Personalizado",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Idiomas disponibles",
"description": "Idiomas que los usuarios pueden seleccionar (déjalo vacío para habilitar todos los idiomas)"
"description": "Idiomas que los usuarios pueden seleccionar (déjalo vacío para habilitar todos los idiomas)",
"placeholder": "Selecciona idiomas"
},
"customMetadata": {
"label": "Metadatos personalizados",
@@ -4061,7 +4108,9 @@
"label": "Estilo del logotipo",
"description": "Elige entre el logotipo moderno minimalista o el icono S clásico",
"classic": "Clásico",
"modern": "Moderno"
"classicAlt": "Logo clásico",
"modern": "Moderno",
"modernAlt": "Logo moderno"
},
"customPaths": {
"label": "Rutas personalizadas",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configuración de la aplicación",
"description": "Ajustes actuales de la aplicación y detalles de configuración."
"description": "Ajustes actuales de la aplicación y detalles de configuración.",
"loading": "Cargando configuración...",
"error": "Error",
"warning": "Advertencia de configuración",
"sections": {
"basic": "Configuración básica",
"security": "Configuración de seguridad",
"system": "Configuración del sistema",
"integration": "Configuración de integración"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gestiona equipos y organiza a los miembros del espacio de trabajo",
"loading": "Cargando equipos...",
"loadingDetails": "Cargando detalles del equipo...",
"loadError": "Error al cargar los detalles del equipo",
"createNewTeam": "Crear nuevo equipo",
"teamName": "Nombre del equipo",
"totalMembers": "Miembros totales",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Preparando tu pago...",
"redirecting": "Redirigiendo al pago seguro...",
"upgradeTitle": "Actualizar a {{planName}}",
"success": "¡Pago realizado con éxito!",
"successMessage": "Tu suscripción se ha activado correctamente. Recibirás un correo de confirmación en breve.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Hobespenak"
},
"workspace": {
"title": "Lan-espazioa",
"people": "Pertsonak",
"teams": "Taldeak"
},
"configuration": {
"title": "Konfigurazioa",
"systemSettings": "Sistemaren ezarpenak",
"features": "Eginbideak",
"endpoints": "Endpoints",
"database": "Datu-basea",
"advanced": "Aurreratua"
},
"securityAuth": {
"title": "Segurtasuna eta Autentifikazioa",
"security": "Segurtasuna",
"connections": "Konexioak"
},
"licensingAnalytics": {
"title": "Lizentziak eta Analitika",
"plan": "Plana",
"audit": "Ikuskaritza",
"usageAnalytics": "Erabilera-analitika"
},
"policiesPrivacy": {
"title": "Politikak eta Pribatutasuna",
"legal": "Legal",
"privacy": "Pribatutasuna"
},
"developer": {
"title": "Garatzailea",
"apiKeys": "API gakoak"
},
"tooltips": {
"enableLoginFirst": "Lehenik login modua gaitu",
"requiresEnterprise": "Enterprise lizentzia behar da"
},
"connection": {
"title": "Konexio modua",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Kendu desgaitutako bihurketa aukerak Convert tresnan gris gisa erakutsi ordez."
},
"hotkeys": {
"title": "Laster-teklak",
"description": "Pertsonalizatu laster-teklak tresnetara azkar sartzeko. Egin klik \"Laster-tekla aldatu\" eta sakatu tekla-konbinazio berria. Sakatu Esc ezeztatzeko.",
"errorConflict": "Laster-tekla dagoeneko {{tool}} tresnak erabiltzen du.",
"errorModifier": {
"mac": "Sartu ⌘ (Command), ⌥ (Option) edo beste aldatzaile bat zure laster-teklean.",
"windows": "Sartu Ctrl, Alt edo beste aldatzaile bat zure laster-teklean."
},
"searchPlaceholder": "Bilatu tresnak...",
"none": "Esleitu gabe",
"customBadge": "Pertsonalizatua",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Hizkuntza erabilgarriak",
"description": "Erabiltzaileek hauta ditzaketen hizkuntzak (utzi hutsik hizkuntza guztiak gaitzeko)"
"description": "Erabiltzaileek hauta ditzaketen hizkuntzak (utzi hutsik hizkuntza guztiak gaitzeko)",
"placeholder": "Hautatu hizkuntzak"
},
"customMetadata": {
"label": "Metadatu pertsonalizatuak",
@@ -4061,7 +4108,9 @@
"label": "Logotipoaren estiloa",
"description": "Aukeratu logotipo moderno eta minimalisten eta S ikono klasikoaren artean",
"classic": "Klasikoa",
"modern": "Modernoa"
"classicAlt": "Logo klasikoa",
"modern": "Modernoa",
"modernAlt": "Logo modernoa"
},
"customPaths": {
"label": "Bide pertsonalizatuak",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Aplikazioaren konfigurazioa",
"description": "Uneko aplikazioaren ezarpenak eta konfigurazio xehetasunak."
"description": "Uneko aplikazioaren ezarpenak eta konfigurazio xehetasunak.",
"loading": "Konfigurazioa kargatzen...",
"error": "Errorea",
"warning": "Konfigurazio-abisoa",
"sections": {
"basic": "Oinarrizko konfigurazioa",
"security": "Segurtasun-konfigurazioa",
"system": "Sistema-konfigurazioa",
"integration": "Integrazio-konfigurazioa"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Kudeatu taldeak eta antolatu lan-espazioko kideak",
"loading": "Taldeak kargatzen...",
"loadingDetails": "Taldearen xehetasunak kargatzen...",
"loadError": "Ezin izan dira taldearen xehetasunak kargatu",
"createNewTeam": "Sortu talde berria",
"teamName": "Taldearen izena",
"totalMembers": "Kideak guztira",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Zure ordainketa prestatzen...",
"redirecting": "Ordainketa segurura birbideratzen...",
"upgradeTitle": "Bertsio-berritu {{planName}} planera",
"success": "Ordainketa arrakastatsua!",
"successMessage": "Zure harpidetza ongi aktibatu da. Laster jasoko duzu baieztapen-mezu elektronikoa.",
+65 -5
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "ترجیحات"
},
"workspace": {
"title": "فضای کاری",
"people": "افراد",
"teams": "تیم‌ها"
},
"configuration": {
"title": "پیکربندی",
"systemSettings": "تنظیمات سیستم",
"features": "قابلیت‌ها",
"endpoints": "نقاط پایانی",
"database": "پایگاه داده",
"advanced": "پیشرفته"
},
"securityAuth": {
"title": "امنیت و احراز هویت",
"security": "امنیت",
"connections": "اتصالات"
},
"licensingAnalytics": {
"title": "مجوزها و آنالیتیکس",
"plan": "طرح",
"audit": "ممیزی",
"usageAnalytics": "آنالیتیکس استفاده"
},
"policiesPrivacy": {
"title": "سیاست‌ها و حریم خصوصی",
"legal": "حقوقی",
"privacy": "حریم خصوصی"
},
"developer": {
"title": "توسعه‌دهنده",
"apiKeys": "کلیدهای API"
},
"tooltips": {
"enableLoginFirst": "ابتدا حالت ورود را فعال کنید",
"requiresEnterprise": "به لایسنس Enterprise نیاز دارد"
},
"connection": {
"title": "حالت اتصال",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "به‌جای نمایش خاکستری، گزینه‌های تبدیل غیرفعال در ابزار Convert را حذف می‌کند."
},
"hotkeys": {
"title": "میانبرهای صفحه‌کلید",
"description": "می‌توانید میانبرهای صفحه‌کلید را برای دسترسی سریع به ابزارها سفارشی کنید. روی «تغییر میانبر» کلیک کنید و ترکیب کلید جدید را فشار دهید. برای لغو، Esc را بزنید.",
"errorConflict": "میانبر قبلاً توسط {{tool}} استفاده شده است.",
"errorModifier": {
"mac": "در میانبر خود ⌘ (Command)، ⌥ (Option) یا یک کلید اصلاحی دیگر را بگنجانید.",
"windows": "در میانبر خود Ctrl، Alt یا یک کلید اصلاحی دیگر را بگنجانید."
},
"searchPlaceholder": "جستجوی ابزارها...",
"none": "اختصاص نیافته",
"customBadge": "سفارشی",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "زبان‌های در دسترس",
"description": "زبان‌هایی که کاربران می‌توانند انتخاب کنند (برای فعال کردن همه زبان‌ها خالی بگذارید)"
"description": "زبان‌هایی که کاربران می‌توانند انتخاب کنند (برای فعال کردن همه زبان‌ها خالی بگذارید)",
"placeholder": "زبان‌ها را انتخاب کنید"
},
"customMetadata": {
"label": "فراداده سفارشی",
@@ -4061,7 +4108,9 @@
"label": "سبک لوگو",
"description": "بین لوگوی مدرن مینیمال یا آیکن کلاسیک S انتخاب کنید",
"classic": "کلاسیک",
"modern": "مدرن"
"classicAlt": "لوگوی کلاسیک",
"modern": "مدرن",
"modernAlt": "لوگوی مدرن"
},
"customPaths": {
"label": "مسیرهای سفارشی",
@@ -4242,7 +4291,7 @@
"description": "ارائه‌دهنده OAuth2 برای احراز هویت"
},
"issuer": {
"label": "Issuer URL",
"label": "URL صادرکننده",
"description": "Issuer URL ارائه‌دهنده OAuth2"
},
"clientId": {
@@ -4471,7 +4520,7 @@
"description": "اجازه به مدیران برای دعوت کاربران از طریق ایمیل با گذرواژه‌های تولید خودکار"
},
"frontendUrl": {
"label": "Frontend URL",
"label": "URL فرانت‌اند",
"description": "URL پایه برای فرانت‌اند (مثلاً https://pdf.example.com). برای ایجاد لینک دعوت در ایمیل‌ها استفاده می‌شود. برای استفاده از URL بک‌اند خالی بگذارید."
}
},
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "پیکربندی برنامه",
"description": "تنظیمات فعلی برنامه و جزئیات پیکربندی."
"description": "تنظیمات فعلی برنامه و جزئیات پیکربندی.",
"loading": "در حال بارگذاری پیکربندی...",
"error": "خطا",
"warning": "هشدار پیکربندی",
"sections": {
"basic": "پیکربندی پایه",
"security": "پیکربندی امنیت",
"system": "پیکربندی سیستم",
"integration": "پیکربندی یکپارچه‌سازی"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "مدیریت تیم‌ها و سازمان‌دهی اعضای فضای کاری",
"loading": "در حال بارگذاری تیم‌ها...",
"loadingDetails": "در حال بارگذاری جزئیات تیم...",
"loadError": "بارگذاری جزئیات تیم ناموفق بود",
"createNewTeam": "ایجاد تیم جدید",
"teamName": "نام تیم",
"totalMembers": "مجموع اعضا",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "در حال آماده‌سازی پرداخت شما...",
"redirecting": "در حال انتقال به تسویه‌حساب امن...",
"upgradeTitle": "ارتقا به {{planName}}",
"success": "پرداخت با موفقیت انجام شد!",
"successMessage": "اشتراک شما با موفقیت فعال شد. به‌زودی ایمیل تأیید دریافت خواهید کرد.",
+65 -5
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Préférences"
},
"workspace": {
"title": "Espace de travail",
"people": "Personnes",
"teams": "Équipes"
},
"configuration": {
"title": "Configuration",
"systemSettings": "Paramètres système",
"features": "Fonctionnalités",
"endpoints": "Endpoints",
"database": "Base de données",
"advanced": "Avancé"
},
"securityAuth": {
"title": "Sécurité et authentification",
"security": "Sécurité",
"connections": "Connexions"
},
"licensingAnalytics": {
"title": "Licences et analyses",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "Analyses d'utilisation"
},
"policiesPrivacy": {
"title": "Politiques et confidentialité",
"legal": "Juridique",
"privacy": "Confidentialité"
},
"developer": {
"title": "Développeur",
"apiKeys": "Clés API"
},
"tooltips": {
"enableLoginFirst": "Activer d'abord le mode connexion",
"requiresEnterprise": "Nécessite une licence Enterprise"
},
"connection": {
"title": "Mode de connexion",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Supprimer les options de conversion désactivées dans loutil Convertir au lieu de les afficher grisées."
},
"hotkeys": {
"title": "Raccourcis clavier",
"description": "Personnalisez les raccourcis clavier pour un accès rapide aux outils. Cliquez sur \"Modifier le raccourci\" et pressez une nouvelle combinaison de touches. Appuyez sur Échap pour annuler.",
"errorConflict": "Raccourci déjà utilisé par {{tool}}.",
"errorModifier": {
"mac": "Incluez ⌘ (Command), ⌥ (Option) ou un autre modificateur dans votre raccourci.",
"windows": "Incluez Ctrl, Alt ou un autre modificateur dans votre raccourci."
},
"searchPlaceholder": "Rechercher des outils…",
"none": "Non attribué",
"customBadge": "Personnalisé",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Langues disponibles",
"description": "Langues parmi lesquelles les utilisateurs peuvent choisir (laisser vide pour activer toutes les langues)"
"description": "Langues parmi lesquelles les utilisateurs peuvent choisir (laisser vide pour activer toutes les langues)",
"placeholder": "Sélectionner des langues"
},
"customMetadata": {
"label": "Métadonnées personnalisées",
@@ -4061,7 +4108,9 @@
"label": "Style de logo",
"description": "Choisir entre le logo minimaliste moderne ou licône S classique",
"classic": "Classique",
"modern": "Moderne"
"classicAlt": "Logo classique",
"modern": "Moderne",
"modernAlt": "Logo moderne"
},
"customPaths": {
"label": "Chemins personnalisés",
@@ -4246,11 +4295,11 @@
"description": "URL de lissuer du fournisseur OAuth2"
},
"clientId": {
"label": "Client ID",
"label": "ID client",
"description": "Le client ID OAuth2 de votre fournisseur"
},
"clientSecret": {
"label": "Client Secret",
"label": "Secret client",
"description": "Le client secret OAuth2 de votre fournisseur"
},
"useAsUsername": {
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configuration de lapplication",
"description": "Paramètres actuels de lapplication et détails de configuration."
"description": "Paramètres actuels de lapplication et détails de configuration.",
"loading": "Chargement de la configuration...",
"error": "Erreur",
"warning": "Avertissement de configuration",
"sections": {
"basic": "Configuration de base",
"security": "Configuration de sécurité",
"system": "Configuration système",
"integration": "Configuration d'intégration"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gérez les équipes et organisez les membres de lespace de travail",
"loading": "Chargement des équipes...",
"loadingDetails": "Chargement des détails de l’équipe...",
"loadError": "Impossible de charger les détails de l'équipe",
"createNewTeam": "Créer une nouvelle équipe",
"teamName": "Nom de l’équipe",
"totalMembers": "Nombre total de membres",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Préparation de votre paiement...",
"redirecting": "Redirection vers le paiement sécurisé...",
"upgradeTitle": "Mettre à niveau vers {{planName}}",
"success": "Paiement réussi !",
"successMessage": "Votre abonnement a été activé avec succès. Vous recevrez un e-mail de confirmation sous peu.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Sainroghanna"
},
"workspace": {
"title": "Spás Oibre",
"people": "Daoine",
"teams": "Foirne"
},
"configuration": {
"title": "Cumraíocht",
"systemSettings": "Socruithe Córais",
"features": "Gnéithe",
"endpoints": "Deirphointí",
"database": "Bunachar Sonraí",
"advanced": "Ardroghanna"
},
"securityAuth": {
"title": "Slándáil agus Fíordheimhniú",
"security": "Slándáil",
"connections": "Naisc"
},
"licensingAnalytics": {
"title": "Ceadúnú agus Anailísíocht",
"plan": "Plean",
"audit": "Iniúchadh",
"usageAnalytics": "Anailísíocht Úsáide"
},
"policiesPrivacy": {
"title": "Polasaithe agus Príobháideachas",
"legal": "Dlíthiúil",
"privacy": "Príobháideachas"
},
"developer": {
"title": "Forbróir",
"apiKeys": "Eochracha API"
},
"tooltips": {
"enableLoginFirst": "Cumasaigh mód logála isteach ar dtús",
"requiresEnterprise": "Teastaíonn ceadúnas Enterprise"
},
"connection": {
"title": "Mód Ceangail",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Bain roghanna tiontaithe díchumasaithe san uirlis Convert seachas iad a thaispeáint i liath."
},
"hotkeys": {
"title": "Aicearraí Méarchláir",
"description": "Saincheap aicearraí méarchláir chun rochtain thapa ar uirlisí. Cliceáil \"Athraigh aicearra\" agus brúigh teaglaim eochracha nua. Brúigh Esc chun cealú.",
"errorConflict": "Tá an t-aicearra in úsáid cheana ag {{tool}}.",
"errorModifier": {
"mac": "Cuir ⌘ (Command), ⌥ (Option), nó modhnóir eile i do aicearra.",
"windows": "Cuir Ctrl, Alt, nó modhnóir eile i do aicearra."
},
"searchPlaceholder": "Cuardaigh uirlisí...",
"none": "Gan sannadh",
"customBadge": "Saincheaptha",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Teangacha ar Fáil",
"description": "Teangacha ar féidir le húsáideoirí a roghnú (fág folamh chun gach teanga a chumasú)"
"description": "Teangacha ar féidir le húsáideoirí a roghnú (fág folamh chun gach teanga a chumasú)",
"placeholder": "Roghnaigh teangacha"
},
"customMetadata": {
"label": "Meiteashonraí Saincheaptha",
@@ -4061,7 +4108,9 @@
"label": "Stíl Lógó",
"description": "Roghnaigh idir an lógó nuaaimseartha íostach nó an tsiombail S chlasaiceach",
"classic": "Clasaiceach",
"modern": "NuaAimseartha"
"classicAlt": "Lógó clasaiceach",
"modern": "NuaAimseartha",
"modernAlt": "Lógó nua-aimseartha"
},
"customPaths": {
"label": "Conairí Saincheaptha",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Cumraíocht an Fheidhmchláir",
"description": "Socruithe reatha an fheidhmchláir agus sonraí cumraíochta."
"description": "Socruithe reatha an fheidhmchláir agus sonraí cumraíochta.",
"loading": "Cumraíocht á luchtú...",
"error": "Earráid",
"warning": "Rabhadh Cumraíochta",
"sections": {
"basic": "Cumraíocht Bhunúsach",
"security": "Cumraíocht Slándála",
"system": "Cumraíocht Córais",
"integration": "Cumraíocht Chomhtháthaithe"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Bainistigh foirne agus eagraigh baill an spáis oibre",
"loading": "Foirne á lódáil...",
"loadingDetails": "Sonraí foirne á lódáil...",
"loadError": "Theip ar luchtú sonraí an fhoirne",
"createNewTeam": "Cruthaigh Foireann Nua",
"teamName": "Ainm Foirne",
"totalMembers": "Iomlán na mBall",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Do sheiceáil amach á ullmhú...",
"redirecting": "Ag atreorú chuig checkout slán...",
"upgradeTitle": "Uasghrádaigh go {{planName}}",
"success": "D’éirigh leis an Íocaíocht!",
"successMessage": "Gníomhachtaíodh do shíntiús go rathúil. Gheobhaidh tú ríomhphost dearbhaithe go luath.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "प्राथमिकताएँ"
},
"workspace": {
"title": "वर्कस्पेस",
"people": "सदस्य",
"teams": "टीमें"
},
"configuration": {
"title": "कॉन्फ़िगरेशन",
"systemSettings": "सिस्टम सेटिंग्स",
"features": "फ़ीचर्स",
"endpoints": "एंडपॉइंट्स",
"database": "डेटाबेस",
"advanced": "एडवांस्ड"
},
"securityAuth": {
"title": "सुरक्षा और प्रमाणीकरण",
"security": "सुरक्षा",
"connections": "कनेक्शन्स"
},
"licensingAnalytics": {
"title": "लाइसेंसिंग और एनालिटिक्स",
"plan": "प्लान",
"audit": "ऑडिट",
"usageAnalytics": "उपयोग एनालिटिक्स"
},
"policiesPrivacy": {
"title": "नीतियाँ और गोपनीयता",
"legal": "कानूनी",
"privacy": "गोपनीयता"
},
"developer": {
"title": "डेवलपर",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "पहले लॉगिन मोड सक्षम करें",
"requiresEnterprise": "Enterprise लाइसेंस आवश्यक"
},
"connection": {
"title": "कनेक्शन मोड",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "कन्वर्ट टूल में अक्षम कन्वर्ज़न विकल्पों को ग्रेआउट दिखाने के बजाय हटा दें।"
},
"hotkeys": {
"title": "कीबोर्ड शॉर्टकट्स",
"description": "त्वरित टूल एक्सेस के लिए कीबोर्ड शॉर्टकट्स कस्टमाइज़ करें। \"Change shortcut\" पर क्लिक करें और नया की कॉम्बिनेशन दबाएँ। रद्द करने के लिए Esc दबाएँ।",
"errorConflict": "शॉर्टकट पहले से {{tool}} द्वारा उपयोग किया गया है।",
"errorModifier": {
"mac": "अपने शॉर्टकट में ⌘ (Command), ⌥ (Option), या कोई अन्य मॉडिफ़ायर शामिल करें।",
"windows": "अपने शॉर्टकट में Ctrl, Alt, या कोई अन्य मॉडिफ़ायर शामिल करें।"
},
"searchPlaceholder": "टूल खोजें...",
"none": "असाइन नहीं",
"customBadge": "कस्टम",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "उपलब्ध भाषाएँ",
"description": "वे भाषाएँ जिन्हें उपयोगकर्ता चुन सकते हैं (सभी भाषाएँ सक्षम करने के लिए खाली छोड़ें)"
"description": "वे भाषाएँ जिन्हें उपयोगकर्ता चुन सकते हैं (सभी भाषाएँ सक्षम करने के लिए खाली छोड़ें)",
"placeholder": "भाषाएँ चुनें"
},
"customMetadata": {
"label": "कस्टम मेटाडाटा",
@@ -4061,7 +4108,9 @@
"label": "लोगो शैली",
"description": "आधुनिक मिनिमलिस्ट लोगो या क्लासिक S आइकन में से चुनें",
"classic": "क्लासिक",
"modern": "आधुनिक"
"classicAlt": "क्लासिक लोगो",
"modern": "आधुनिक",
"modernAlt": "आधुनिक लोगो"
},
"customPaths": {
"label": "कस्टम पाथ",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "एप्लिकेशन कॉन्फ़िगरेशन",
"description": "वर्तमान एप्लिकेशन सेटिंग्स और कॉन्फ़िगरेशन विवरण।"
"description": "वर्तमान एप्लिकेशन सेटिंग्स और कॉन्फ़िगरेशन विवरण।",
"loading": "कॉन्फ़िगरेशन लोड हो रहा है...",
"error": "त्रुटि",
"warning": "कॉन्फ़िगरेशन चेतावनी",
"sections": {
"basic": "बेसिक कॉन्फ़िगरेशन",
"security": "सुरक्षा कॉन्फ़िगरेशन",
"system": "सिस्टम कॉन्फ़िगरेशन",
"integration": "इंटीग्रेशन कॉन्फ़िगरेशन"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "टीम्स प्रबंधित करें और वर्कस्पेस सदस्यों को व्यवस्थित करें",
"loading": "टीम्स लोड हो रही हैं...",
"loadingDetails": "टीम विवरण लोड हो रहे हैं...",
"loadError": "टीम विवरण लोड करने में विफल",
"createNewTeam": "नई टीम बनाएँ",
"teamName": "टीम का नाम",
"totalMembers": "कुल सदस्य",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "आपका चेकआउट तैयार किया जा रहा है...",
"redirecting": "सुरक्षित चेकआउट पर रीडायरेक्ट किया जा रहा है...",
"upgradeTitle": "{{planName}} में अपग्रेड करें",
"success": "भुगतान सफल!",
"successMessage": "आपकी सदस्यता सफलतापूर्वक सक्रिय कर दी गई है। आपको जल्द ही एक पुष्टि ईमेल प्राप्त होगा।",
+68 -8
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferencije"
},
"workspace": {
"title": "Radni prostor",
"people": "Korisnici",
"teams": "Timovi"
},
"configuration": {
"title": "Konfiguracija",
"systemSettings": "Postavke sustava",
"features": "Značajke",
"endpoints": "Endpointi",
"database": "Baza podataka",
"advanced": "Napredno"
},
"securityAuth": {
"title": "Sigurnost i autentikacija",
"security": "Sigurnost",
"connections": "Povezivanja"
},
"licensingAnalytics": {
"title": "Licenciranje i analitika",
"plan": "Plan",
"audit": "Revizija",
"usageAnalytics": "Analitika korištenja"
},
"policiesPrivacy": {
"title": "Pravila i privatnost",
"legal": "Pravno",
"privacy": "Privatnost"
},
"developer": {
"title": "Razvoj",
"apiKeys": "API ključevi"
},
"tooltips": {
"enableLoginFirst": "Najprije omogućite način prijave",
"requiresEnterprise": "Zahtijeva Enterprise licencu"
},
"connection": {
"title": "Način povezivanja",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Uklonite onemogućene opcije pretvorbe u alatu Pretvori umjesto da su zasivljene."
},
"hotkeys": {
"title": "Prečaci tipkovnice",
"description": "Prilagodite prečace tipkovnice za brz pristup alatima. Kliknite \"Promijeni prečac\" i pritisnite novu kombinaciju tipki. Pritisnite Esc za odustajanje.",
"errorConflict": "Prečac već koristi {{tool}}.",
"errorModifier": {
"mac": "Uključite ⌘ (Command), ⌥ (Option) ili neki drugi modifikator u svom prečacu.",
"windows": "Uključite Ctrl, Alt ili neki drugi modifikator u svom prečacu."
},
"searchPlaceholder": "Pretraži alate...",
"none": "Nije dodijeljeno",
"customBadge": "Prilagođeno",
@@ -2697,7 +2743,7 @@
"selectPDF": "Odaberite datoteku PDF:",
"submit": "Ukloni potpisi",
"description": "Ovaj alat će ukloniti potpise digitalnim certifikatom iz vašeg PDF dokumenta.",
"filenamePrefix": "unsigned",
"filenamePrefix": "nepotpisano",
"files": {
"placeholder": "Odaberite PDF datoteku u glavnom prikazu za početak"
},
@@ -3547,7 +3593,7 @@
"header": "PDF u Jednu Stranicu",
"submit": "Pretvori u Jednu Stranicu",
"description": "Ovaj alat će spojiti sve stranice vašeg PDF-a u jednu veliku stranicu. Širina će ostati ista kao izvorna, a visina će biti zbroj visina svih stranica.",
"filenamePrefix": "single_page",
"filenamePrefix": "jedna_stranica",
"files": {
"placeholder": "Odaberite PDF datoteku u glavnom prikazu za početak"
},
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Dostupni jezici",
"description": "Jezici koje korisnici mogu odabrati (ostavite prazno za omogućavanje svih jezika)"
"description": "Jezici koje korisnici mogu odabrati (ostavite prazno za omogućavanje svih jezika)",
"placeholder": "Odaberite jezike"
},
"customMetadata": {
"label": "Prilagođeni metapodaci",
@@ -4061,7 +4108,9 @@
"label": "Stil logotipa",
"description": "Odaberite između modernog minimalističkog logotipa ili klasične S ikone",
"classic": "Klasični",
"modern": "Moderni"
"classicAlt": "Klasični logotip",
"modern": "Moderni",
"modernAlt": "Moderni logotip"
},
"customPaths": {
"label": "Prilagođene putanje",
@@ -4246,11 +4295,11 @@
"description": "Issuer URL OAuth2 pružatelja"
},
"clientId": {
"label": "Client ID",
"label": "ID klijenta",
"description": "OAuth2 Client ID s vašeg pružatelja"
},
"clientSecret": {
"label": "Client Secret",
"label": "Tajna klijenta",
"description": "OAuth2 Client Secret s vašeg pružatelja"
},
"useAsUsername": {
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfiguracija aplikacije",
"description": "Trenutačne postavke i pojedinosti konfiguracije aplikacije."
"description": "Trenutačne postavke i pojedinosti konfiguracije aplikacije.",
"loading": "Učitavanje konfiguracije...",
"error": "Pogreška",
"warning": "Upozorenje konfiguracije",
"sections": {
"basic": "Osnovna konfiguracija",
"security": "Sigurnosna konfiguracija",
"system": "Konfiguracija sustava",
"integration": "Konfiguracija integracije"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Upravljajte timovima i organizirajte članove radnog prostora",
"loading": "Učitavanje timova...",
"loadingDetails": "Učitavanje pojedinosti o timu...",
"loadError": "Nije uspjelo učitavanje detalja tima",
"createNewTeam": "Stvori novi tim",
"teamName": "Naziv tima",
"totalMembers": "Ukupno članova",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Priprema naplate...",
"redirecting": "Preusmjeravanje na sigurnu naplatu...",
"upgradeTitle": "Nadogradi na {{planName}}",
"success": "Plaćanje uspješno!",
"successMessage": "Vaša je pretplata uspješno aktivirana. Uskoro ćete primiti potvrdni e-mail.",
@@ -6035,7 +6095,7 @@
"error": {
"failed": "Došlo je do pogreške pri dodavanju teksta u PDF."
},
"tags": "text,annotation,label",
"tags": "tekst,bilješka,oznaka",
"applySignatures": "Primijeni tekst"
}
}
+65 -5
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Beállítások"
},
"workspace": {
"title": "Munkaterület",
"people": "Felhasználók",
"teams": "Csapatok"
},
"configuration": {
"title": "Konfiguráció",
"systemSettings": "Rendszerbeállítások",
"features": "Funkciók",
"endpoints": "Végpontok",
"database": "Adatbázis",
"advanced": "Speciális"
},
"securityAuth": {
"title": "Biztonság és hitelesítés",
"security": "Biztonság",
"connections": "Kapcsolatok"
},
"licensingAnalytics": {
"title": "Licencelés és analitika",
"plan": "Csomag",
"audit": "Audit",
"usageAnalytics": "Használati analitika"
},
"policiesPrivacy": {
"title": "Irányelvek és adatvédelem",
"legal": "Jogi",
"privacy": "Adatvédelem"
},
"developer": {
"title": "Fejlesztő",
"apiKeys": "API-kulcsok"
},
"tooltips": {
"enableLoginFirst": "Először engedélyezze a bejelentkezési módot",
"requiresEnterprise": "Enterprise licenc szükséges"
},
"connection": {
"title": "Kapcsolati mód",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "A Konvertálás eszközben a letiltott opciók eltávolítása, ahelyett hogy szürkén jelenjenek meg."
},
"hotkeys": {
"title": "Billentyűparancsok",
"description": "Testreszabhatja a billentyűparancsokat az eszközök gyors eléréséhez. Kattintson a \"Billentyűparancs módosítása\" gombra, és nyomjon meg egy új billentyűkombinációt. A megszakításhoz nyomja meg az Esc billentyűt.",
"errorConflict": "Ezt a gyorsbillentyűt már használja: {{tool}}.",
"errorModifier": {
"mac": "A billentyűparancs tartalmazzon ⌘ (Command), ⌥ (Option) vagy más módosítóbillentyűt.",
"windows": "A billentyűparancs tartalmazzon Ctrl, Alt vagy más módosítóbillentyűt."
},
"searchPlaceholder": "Eszközök keresése...",
"none": "Nincs hozzárendelve",
"customBadge": "Egyéni",
@@ -1125,7 +1171,7 @@
"settings": "Beállítások",
"conversionCompleted": "Átalakítás befejezve",
"results": "Eredmények",
"defaultFilename": "converted_file",
"defaultFilename": "konvertalt_fajl",
"conversionResults": "Átalakítás eredménye",
"convertFrom": "Átalakítás erről",
"convertTo": "Átalakítás erre",
@@ -3547,7 +3593,7 @@
"header": "PDF egyoldalassá alakítása",
"submit": "Konvertálás egyoldalassá",
"description": "Ez az eszköz a PDF összes oldalát egy nagy, egyetlen oldallá egyesíti. A szélesség megegyezik az eredeti oldalakéval, a magasság pedig az összes oldalmagasság összege lesz.",
"filenamePrefix": "single_page",
"filenamePrefix": "egyetlen_oldal",
"files": {
"placeholder": "A kezdéshez válasszon ki egy PDF-fájlt a fő nézetben"
},
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Elérhető nyelvek",
"description": "A felhasználók által választható nyelvek (hagyja üresen az összes nyelv engedélyezéséhez)"
"description": "A felhasználók által választható nyelvek (hagyja üresen az összes nyelv engedélyezéséhez)",
"placeholder": "Válasszon nyelveket"
},
"customMetadata": {
"label": "Egyéni metaadatok",
@@ -4061,7 +4108,9 @@
"label": "Logó stílusa",
"description": "Válasszon a modern minimalista logó és a klasszikus S ikon között",
"classic": "Klasszikus",
"modern": "Modern"
"classicAlt": "Klasszikus logó",
"modern": "Modern",
"modernAlt": "Modern logó"
},
"customPaths": {
"label": "Egyéni elérési utak",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Alkalmazás konfigurációja",
"description": "Az alkalmazás aktuális beállításai és konfigurációs részletei."
"description": "Az alkalmazás aktuális beállításai és konfigurációs részletei.",
"loading": "Konfiguráció betöltése...",
"error": "Hiba",
"warning": "Konfigurációs figyelmeztetés",
"sections": {
"basic": "Alapkonfiguráció",
"security": "Biztonsági konfiguráció",
"system": "Rendszerkonfiguráció",
"integration": "Integrációs konfiguráció"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Csapatok kezelése és a munkaterület tagjainak rendszerezése",
"loading": "Csapatok betöltése...",
"loadingDetails": "Csapat részleteinek betöltése...",
"loadError": "Nem sikerült betölteni a csapat adatait",
"createNewTeam": "Új csapat létrehozása",
"teamName": "Csapat neve",
"totalMembers": "Tagok összesen",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Fizetés előkészítése...",
"redirecting": "Átirányítás a biztonságos pénztárhoz...",
"upgradeTitle": "Frissítés erre: {{planName}}",
"success": "Sikeres fizetés!",
"successMessage": "Az előfizetése sikeresen aktiválva. Hamarosan megerősítő e-mailt kap.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferensi"
},
"workspace": {
"title": "Ruang kerja",
"people": "Anggota",
"teams": "Tim"
},
"configuration": {
"title": "Konfigurasi",
"systemSettings": "Pengaturan Sistem",
"features": "Fitur",
"endpoints": "Endpoint",
"database": "Database",
"advanced": "Lanjutan"
},
"securityAuth": {
"title": "Keamanan & Autentikasi",
"security": "Keamanan",
"connections": "Koneksi"
},
"licensingAnalytics": {
"title": "Lisensi & Analitik",
"plan": "Paket",
"audit": "Audit",
"usageAnalytics": "Analitik Penggunaan"
},
"policiesPrivacy": {
"title": "Kebijakan & Privasi",
"legal": "Legal",
"privacy": "Privasi"
},
"developer": {
"title": "Pengembang",
"apiKeys": "Kunci API"
},
"tooltips": {
"enableLoginFirst": "Aktifkan mode login terlebih dahulu",
"requiresEnterprise": "Memerlukan lisensi Enterprise"
},
"connection": {
"title": "Mode Koneksi",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Hapus opsi konversi yang dinonaktifkan di alat Convert alih-alih menampilkannya pudar."
},
"hotkeys": {
"title": "Pintasan Keyboard",
"description": "Sesuaikan pintasan keyboard untuk akses cepat ke alat. Klik \"Change shortcut\" dan tekan kombinasi tombol baru. Tekan Esc untuk membatalkan.",
"errorConflict": "Pintasan sudah digunakan oleh {{tool}}.",
"errorModifier": {
"mac": "Sertakan ⌘ (Command), ⌥ (Option), atau modifier lainnya dalam pintasan Anda.",
"windows": "Sertakan Ctrl, Alt, atau modifier lainnya dalam pintasan Anda."
},
"searchPlaceholder": "Cari alat...",
"none": "Tidak ditetapkan",
"customBadge": "Kustom",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Bahasa yang Tersedia",
"description": "Bahasa yang dapat dipilih pengguna (biarkan kosong untuk mengaktifkan semua bahasa)"
"description": "Bahasa yang dapat dipilih pengguna (biarkan kosong untuk mengaktifkan semua bahasa)",
"placeholder": "Pilih bahasa"
},
"customMetadata": {
"label": "Metadata Kustom",
@@ -4061,7 +4108,9 @@
"label": "Gaya Logo",
"description": "Pilih antara logo minimalis modern atau ikon S klasik",
"classic": "Klasik",
"modern": "Modern"
"classicAlt": "Logo klasik",
"modern": "Modern",
"modernAlt": "Logo modern"
},
"customPaths": {
"label": "Jalur Kustom",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfigurasi Aplikasi",
"description": "Pengaturan dan detail konfigurasi aplikasi saat ini."
"description": "Pengaturan dan detail konfigurasi aplikasi saat ini.",
"loading": "Memuat konfigurasi...",
"error": "Error",
"warning": "Peringatan Konfigurasi",
"sections": {
"basic": "Konfigurasi Dasar",
"security": "Konfigurasi Keamanan",
"system": "Konfigurasi Sistem",
"integration": "Konfigurasi Integrasi"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Kelola tim dan atur anggota ruang kerja",
"loading": "Memuat tim...",
"loadingDetails": "Memuat rincian tim...",
"loadError": "Gagal memuat detail tim",
"createNewTeam": "Buat Tim Baru",
"teamName": "Nama Tim",
"totalMembers": "Total Anggota",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Menyiapkan checkout Anda...",
"redirecting": "Mengalihkan ke checkout aman...",
"upgradeTitle": "Tingkatkan ke {{planName}}",
"success": "Pembayaran Berhasil!",
"successMessage": "Langganan Anda telah berhasil diaktifkan. Anda akan segera menerima email konfirmasi.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferenze"
},
"workspace": {
"title": "Workspace",
"people": "Persone",
"teams": "Team"
},
"configuration": {
"title": "Configurazione",
"systemSettings": "Impostazioni di sistema",
"features": "Funzionalità",
"endpoints": "Endpoint",
"database": "Database",
"advanced": "Avanzate"
},
"securityAuth": {
"title": "Sicurezza e autenticazione",
"security": "Sicurezza",
"connections": "Connessioni"
},
"licensingAnalytics": {
"title": "Licenze e Analytics",
"plan": "Piano",
"audit": "Audit",
"usageAnalytics": "Analytics d'uso"
},
"policiesPrivacy": {
"title": "Policy e privacy",
"legal": "Legale",
"privacy": "Privacy"
},
"developer": {
"title": "Sviluppatore",
"apiKeys": "Chiavi API"
},
"tooltips": {
"enableLoginFirst": "Abilita prima la modalità di login",
"requiresEnterprise": "Richiede licenza Enterprise"
},
"connection": {
"title": "Modalità di connessione",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Rimuovi le opzioni di conversione disabilitate nello strumento Converti invece di mostrarle in grigio."
},
"hotkeys": {
"title": "Scorciatoie da tastiera",
"description": "Personalizza le scorciatoie da tastiera per l'accesso rapido agli strumenti. Clicca \"Cambia scorciatoia\" e premi una nuova combinazione di tasti. Premi Esc per annullare.",
"errorConflict": "Scorciatoia già usata da {{tool}}.",
"errorModifier": {
"mac": "Includi ⌘ (Command), ⌥ (Option) o un altro modificatore nella scorciatoia.",
"windows": "Includi Ctrl, Alt o un altro modificatore nella scorciatoia."
},
"searchPlaceholder": "Cerca strumenti...",
"none": "Non assegnata",
"customBadge": "Personalizzata",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Lingue disponibili",
"description": "Lingue selezionabili dagli utenti (lascia vuoto per abilitare tutte le lingue)"
"description": "Lingue selezionabili dagli utenti (lascia vuoto per abilitare tutte le lingue)",
"placeholder": "Seleziona lingue"
},
"customMetadata": {
"label": "Metadati personalizzati",
@@ -4061,7 +4108,9 @@
"label": "Stile logo",
"description": "Scegli tra il logo moderno minimalista o l'icona classica S",
"classic": "Classico",
"modern": "Moderno"
"classicAlt": "Logo classico",
"modern": "Moderno",
"modernAlt": "Logo moderno"
},
"customPaths": {
"label": "Percorsi personalizzati",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configurazione applicazione",
"description": "Impostazioni correnti dell'applicazione e dettagli di configurazione."
"description": "Impostazioni correnti dell'applicazione e dettagli di configurazione.",
"loading": "Caricamento configurazione...",
"error": "Errore",
"warning": "Avviso configurazione",
"sections": {
"basic": "Configurazione di base",
"security": "Configurazione sicurezza",
"system": "Configurazione sistema",
"integration": "Configurazione integrazione"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gestisci i team e organizza i membri dello spazio di lavoro",
"loading": "Caricamento dei team...",
"loadingDetails": "Caricamento dei dettagli del team...",
"loadError": "Impossibile caricare i dettagli del team",
"createNewTeam": "Crea nuovo team",
"teamName": "Nome team",
"totalMembers": "Membri totali",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Preparazione del checkout...",
"redirecting": "Reindirizzamento al checkout sicuro...",
"upgradeTitle": "Aggiorna a {{planName}}",
"success": "Pagamento riuscito!",
"successMessage": "Il tuo abbonamento è stato attivato con successo. Riceverai a breve un'email di conferma.",
+65 -5
View File
@@ -29,7 +29,7 @@
"fullscreen": "フルスクリーンモードに切り替え",
"sidebar": "サイドバーモードに切り替え"
},
"alpha": "Alpha",
"alpha": "アルファ",
"premiumFeature": "プレミアム機能:",
"comingSoon": "近日公開:"
},
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "環境設定"
},
"workspace": {
"title": "ワークスペース",
"people": "メンバー",
"teams": "チーム"
},
"configuration": {
"title": "構成",
"systemSettings": "システム設定",
"features": "機能",
"endpoints": "エンドポイント",
"database": "データベース",
"advanced": "詳細設定"
},
"securityAuth": {
"title": "セキュリティと認証",
"security": "セキュリティ",
"connections": "接続"
},
"licensingAnalytics": {
"title": "ライセンスと分析",
"plan": "プラン",
"audit": "監査",
"usageAnalytics": "利用分析"
},
"policiesPrivacy": {
"title": "ポリシーとプライバシー",
"legal": "法務",
"privacy": "プライバシー"
},
"developer": {
"title": "開発者",
"apiKeys": "APIキー"
},
"tooltips": {
"enableLoginFirst": "先にログインモードを有効にしてください",
"requiresEnterprise": "Enterpriseライセンスが必要です"
},
"connection": {
"title": "接続モード",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "グレー表示の代わりに、Convert ツールで無効な変換オプションを非表示にします。"
},
"hotkeys": {
"title": "キーボードショートカット",
"description": "ツールにすばやくアクセスできるようキーボードショートカットをカスタマイズします。「Change shortcut」をクリックし、新しいキーの組み合わせを押してください。Escでキャンセルします。",
"errorConflict": "ショートカットはすでに{{tool}}で使用されています。",
"errorModifier": {
"mac": "ショートカットに⌘(Command)、⌥(Option)などの修飾キーを含めてください。",
"windows": "ショートカットにCtrl、Altなどの修飾キーを含めてください。"
},
"searchPlaceholder": "ツールを検索...",
"none": "未割り当て",
"customBadge": "カスタム",
@@ -1867,7 +1913,7 @@
},
"trapped": {
"label": "トラッピング状態",
"unknown": "Unknown",
"unknown": "不明",
"true": "True",
"false": "False"
},
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "利用可能な言語",
"description": "ユーザーが選択できる言語(空にすると全言語を有効化)"
"description": "ユーザーが選択できる言語(空にすると全言語を有効化)",
"placeholder": "言語を選択"
},
"customMetadata": {
"label": "カスタムメタデータ",
@@ -4061,7 +4108,9 @@
"label": "ロゴスタイル",
"description": "最新のミニマリストロゴかクラシックな S アイコンを選択",
"classic": "クラシック",
"modern": "モダン"
"classicAlt": "クラシックロゴ",
"modern": "モダン",
"modernAlt": "モダンロゴ"
},
"customPaths": {
"label": "カスタムパス",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "アプリケーション構成",
"description": "現在のアプリケーション設定と構成の詳細。"
"description": "現在のアプリケーション設定と構成の詳細。",
"loading": "構成を読み込み中...",
"error": "エラー",
"warning": "構成の警告",
"sections": {
"basic": "基本構成",
"security": "セキュリティ構成",
"system": "システム構成",
"integration": "統合構成"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "チームを管理し、ワークスペースのメンバーを整理します",
"loading": "チームを読み込み中...",
"loadingDetails": "チームの詳細を読み込み中...",
"loadError": "チームの詳細を読み込めませんでした",
"createNewTeam": "新しいチームを作成",
"teamName": "チーム名",
"totalMembers": "メンバー総数",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "チェックアウトを準備しています...",
"redirecting": "安全なチェックアウトにリダイレクトしています...",
"upgradeTitle": "{{planName}} にアップグレード",
"success": "支払いが完了しました",
"successMessage": "サブスクリプションが有効化されました。まもなく確認メールが届きます。",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "환경설정"
},
"workspace": {
"title": "워크스페이스",
"people": "구성원",
"teams": "팀"
},
"configuration": {
"title": "구성",
"systemSettings": "시스템 설정",
"features": "기능",
"endpoints": "엔드포인트",
"database": "데이터베이스",
"advanced": "고급"
},
"securityAuth": {
"title": "보안 및 인증",
"security": "보안",
"connections": "연결"
},
"licensingAnalytics": {
"title": "라이선스 및 분석",
"plan": "플랜",
"audit": "감사",
"usageAnalytics": "사용 분석"
},
"policiesPrivacy": {
"title": "정책 및 개인정보 보호",
"legal": "법률",
"privacy": "개인정보 보호"
},
"developer": {
"title": "개발자",
"apiKeys": "API 키"
},
"tooltips": {
"enableLoginFirst": "먼저 로그인 모드를 활성화하세요",
"requiresEnterprise": "Enterprise 라이선스가 필요합니다"
},
"connection": {
"title": "연결 모드",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "회색으로 표시하는 대신 변환 도구의 비활성화된 옵션을 숨깁니다."
},
"hotkeys": {
"title": "키보드 단축키",
"description": "빠르게 도구에 접근할 수 있도록 키보드 단축키를 사용자 지정하세요. \"단축키 변경\"을 클릭하고 새 키 조합을 누르세요. 취소하려면 Esc를 누르세요.",
"errorConflict": "단축키가 {{tool}}에서 이미 사용 중입니다.",
"errorModifier": {
"mac": "단축키에 ⌘(Command), ⌥(Option) 또는 다른 수정 키를 포함하세요.",
"windows": "단축키에 Ctrl, Alt 또는 다른 수정 키를 포함하세요."
},
"searchPlaceholder": "도구 검색...",
"none": "미지정",
"customBadge": "사용자 지정",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "사용 가능한 언어",
"description": "사용자가 선택할 수 있는 언어(비워두면 모든 언어 활성화)"
"description": "사용자가 선택할 수 있는 언어(비워두면 모든 언어 활성화)",
"placeholder": "언어 선택"
},
"customMetadata": {
"label": "사용자 지정 메타데이터",
@@ -4061,7 +4108,9 @@
"label": "로고 스타일",
"description": "현대적인 미니멀 로고 또는 클래식 S 아이콘 중에서 선택",
"classic": "클래식",
"modern": "모던"
"classicAlt": "클래식 로고",
"modern": "모던",
"modernAlt": "모던 로고"
},
"customPaths": {
"label": "사용자 지정 경로",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "애플리케이션 구성",
"description": "현재 애플리케이션 설정 및 구성 세부 정보입니다."
"description": "현재 애플리케이션 설정 및 구성 세부 정보입니다.",
"loading": "구성을 불러오는 중...",
"error": "오류",
"warning": "구성 경고",
"sections": {
"basic": "기본 구성",
"security": "보안 구성",
"system": "시스템 구성",
"integration": "통합 구성"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "팀을 관리하고 워크스페이스 멤버를 구성하세요",
"loading": "팀 불러오는 중...",
"loadingDetails": "팀 세부 정보를 불러오는 중...",
"loadError": "팀 세부 정보를 불러오지 못했습니다",
"createNewTeam": "새 팀 만들기",
"teamName": "팀 이름",
"totalMembers": "총 멤버 수",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "결제 준비 중...",
"redirecting": "보안 결제 페이지로 이동 중...",
"upgradeTitle": "{{planName}}로 업그레이드",
"success": "결제 성공!",
"successMessage": "구독이 성공적으로 활성화되었습니다. 확인 이메일이 곧 발송됩니다.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "അഭിരുചികൾ"
},
"workspace": {
"title": "വർക്ക്‌സ്പേസ്",
"people": "അംഗങ്ങൾ",
"teams": "ടീമുകൾ"
},
"configuration": {
"title": "കോൺഫിഗറേഷൻ",
"systemSettings": "സിസ്റ്റം ക്രമീകരണങ്ങൾ",
"features": "ഫീച്ചറുകൾ",
"endpoints": "എൻഡ്പോയിന്റുകൾ",
"database": "ഡാറ്റാബേസ്",
"advanced": "അഡ്വാൻസ്ഡ്"
},
"securityAuth": {
"title": "സുരക്ഷയും ഓതന്റിക്കേഷനും",
"security": "സുരക്ഷ",
"connections": "കണക്ഷനുകൾ"
},
"licensingAnalytics": {
"title": "ലൈസൻസിംഗും അനലിറ്റിക്സും",
"plan": "പ്ലാൻ",
"audit": "ഓഡിറ്റ്",
"usageAnalytics": "ഉപയോഗ അനലിറ്റിക്സ്"
},
"policiesPrivacy": {
"title": "നയങ്ങളും സ്വകാര്യതയും",
"legal": "ലീഗൽ",
"privacy": "സ്വകാര്യത"
},
"developer": {
"title": "ഡെവലപ്പർ",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "ആദ്യം ലോഗിൻ മോഡ് സജീവമാക്കുക",
"requiresEnterprise": "എന്റർപ്രൈസ് ലൈസൻസ് ആവശ്യമാണ്"
},
"connection": {
"title": "കണക്ഷൻ മോഡ്",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "ഗ്രേ ഔട്ട് ചെയ്യുന്നതിനുപകരം Convert ടൂളിലെ പ്രവർത്തനരഹിതമായ ഓപ്ഷനുകൾ നീക്കം ചെയ്യുക."
},
"hotkeys": {
"title": "കീബോർഡ് ഷോർട്ട്‌കട്ടുകൾ",
"description": "ടൂൾസ് വേഗത്തിൽ ആക്‌സസ് ചെയ്യാൻ കീബോർഡ് ഷോർട്ട്‌കട്ടുകൾ ഇഷ്ടാനുസൃതമാക്കുക. \"Change shortcut\" ക്ലിക്ക് ചെയ്ത് ഒരു പുതിയ കീ കോംബിനേഷൻ അമർത്തുക. റദ്ദാക്കാൻ Esc അമർത്തുക.",
"errorConflict": "ഷോർട്ട്‌കട്ട് {{tool}} ഇതിനകം ഉപയോഗിക്കുന്നു.",
"errorModifier": {
"mac": "നിങ്ങളുടെ ഷോർട്ട്‌കട്ടിൽ ⌘ (Command), ⌥ (Option) അല്ലെങ്കിൽ മറ്റൊരു മൊഡിഫയർ ഉൾപ്പെടുത്തുക.",
"windows": "നിങ്ങളുടെ ഷോർട്ട്‌കട്ടിൽ Ctrl, Alt അല്ലെങ്കിൽ മറ്റൊരു മൊഡിഫയർ ഉൾപ്പെടുത്തുക."
},
"searchPlaceholder": "ടൂളുകൾ തിരയുക...",
"none": "നിയോഗിച്ചിട്ടില്ല",
"customBadge": "കസ്റ്റം",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "ലഭ്യമായ ഭാഷകൾ",
"description": "ഉപയോക്താക്കൾക്ക് തിരഞ്ഞെടുക്കാൻ കഴിയുന്ന ഭാഷകൾ (എല്ലാ ഭാഷകളും പ്രാപ്തമാക്കാൻ ശൂന്യം വിടുക)"
"description": "ഉപയോക്താക്കൾക്ക് തിരഞ്ഞെടുക്കാൻ കഴിയുന്ന ഭാഷകൾ (എല്ലാ ഭാഷകളും പ്രാപ്തമാക്കാൻ ശൂന്യം വിടുക)",
"placeholder": "ഭാഷകൾ തിരഞ്ഞെടുക്കുക"
},
"customMetadata": {
"label": "കസ്റ്റം മെറ്റാഡാറ്റ",
@@ -4061,7 +4108,9 @@
"label": "ലോഗോ ശൈലി",
"description": "ആധുനിക മിനിമലിസ്റ്റ് ലോഗോയും ക്ലാസിക് S ഐക്കണും തമ്മിൽ തിരഞ്ഞെടുക്കുക",
"classic": "ക്ലാസിക്",
"modern": "മോഡേൺ"
"classicAlt": "ക്ലാസിക് ലോഗോ",
"modern": "മോഡേൺ",
"modernAlt": "മോഡേൺ ലോഗോ"
},
"customPaths": {
"label": "കസ്റ്റം പാതകൾ",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "ആപ്ലിക്കേഷൻ ക്രമീകരണം",
"description": "നിലവിലെ ആപ്ലിക്കേഷൻ ക്രമീകരണങ്ങളും കോൺഫിഗറേഷൻ വിശദാംശങ്ങളും."
"description": "നിലവിലെ ആപ്ലിക്കേഷൻ ക്രമീകരണങ്ങളും കോൺഫിഗറേഷൻ വിശദാംശങ്ങളും.",
"loading": "കോൺഫിഗറേഷൻ ലോഡുചെയ്യുന്നു...",
"error": "പിശക്",
"warning": "കോൺഫിഗറേഷൻ മുന്നറിയിപ്പ്",
"sections": {
"basic": "ബേസിക് കോൺഫിഗറേഷൻ",
"security": "സുരക്ഷാ കോൺഫിഗറേഷൻ",
"system": "സിസ്റ്റം കോൺഫിഗറേഷൻ",
"integration": "ഇന്റഗ്രേഷൻ കോൺഫിഗറേഷൻ"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "ടീമുകളെ നിയന്ത്രിക്കുകയും വർക്ക്സ്പേസ് അംഗങ്ങളെ ക്രമപ്പെടുത്തിയതാക്കുകയും ചെയ്യുക",
"loading": "ടീമുകൾ ലോഡ് ചെയ്യുന്നു...",
"loadingDetails": "ടീം വിശദാംശങ്ങൾ ലോഡ് ചെയ്യുന്നു...",
"loadError": "ടീം വിശദാംശങ്ങൾ ലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു",
"createNewTeam": "പുതിയ ടീം സൃഷ്ടിക്കുക",
"teamName": "ടീം പേര്",
"totalMembers": "ആകെ അംഗങ്ങൾ",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "നിങ്ങളുടെ ചെക്ക്ഔട്ട് തയ്യാറാക്കുന്നു...",
"redirecting": "സുരക്ഷിത checkout ലേക്ക് റീഡിറക്ട് ചെയ്യുന്നു...",
"upgradeTitle": "{{planName}} ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക",
"success": "പേയ്മെന്റ് വിജയകരം!",
"successMessage": "നിങ്ങളുടെ സബ്‌സ്‌ക്രിപ്ഷൻ വിജയകരമായി സജീവമാക്കിയിരിക്കുന്നു. ഉടൻ തന്നെ നിങ്ങൾക്ക് ഒരു സ്ഥിരീകരണ ഇമെയിൽ ലഭിക്കും.",
@@ -5568,7 +5628,7 @@
"yearly": "വർഷം",
"billingPeriod": "ബില്ലിംഗ് കാലയളവ്",
"enterpriseNote": "ചെക്കൗട്ടിൽ സീറ്റുകൾ ക്രമീകരിക്കാം (1-1000).",
"installationId": "Installation ID",
"installationId": "ഇൻസ്റ്റാളേഷൻ ID",
"licenseKey": "നിങ്ങളുടെ ലൈസൻസ് കീ",
"licenseInstructions": "ഇത് നിങ്ങളുടെ ഇൻസ്റ്റലേഷനിൽ ചേർത്തിരിക്കുന്നു. നിങ്ങളുടെ ഇമെയിലിലും ഒരു പകർപ്പ് ലഭിക്കും.",
"canCloseWindow": "ഇപ്പോൾ ഈ വിൻഡോ അടയ്ക്കാം.",
+68 -8
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Voorkeuren"
},
"workspace": {
"title": "Werkruimte",
"people": "Personen",
"teams": "Teams"
},
"configuration": {
"title": "Configuratie",
"systemSettings": "Systeeminstellingen",
"features": "Functies",
"endpoints": "Endpoints",
"database": "Database",
"advanced": "Geavanceerd"
},
"securityAuth": {
"title": "Beveiliging & Authenticatie",
"security": "Beveiliging",
"connections": "Verbindingen"
},
"licensingAnalytics": {
"title": "Licenties & Analytics",
"plan": "Abonnement",
"audit": "Audit",
"usageAnalytics": "Gebruiksstatistieken"
},
"policiesPrivacy": {
"title": "Beleid & Privacy",
"legal": "Juridisch",
"privacy": "Privacy"
},
"developer": {
"title": "Ontwikkelaar",
"apiKeys": "API-sleutels"
},
"tooltips": {
"enableLoginFirst": "Schakel eerst de loginmodus in",
"requiresEnterprise": "Vereist Enterprise-licentie"
},
"connection": {
"title": "Verbindingsmodus",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Verwijder uitgeschakelde conversie-opties in de tool Converteren in plaats van ze grijs te tonen."
},
"hotkeys": {
"title": "Sneltoetsen",
"description": "Pas sneltoetsen aan voor snelle toegang tot tools. Klik op \"Sneltoets wijzigen\" en druk een nieuwe toetsencombinatie in. Druk op Esc om te annuleren.",
"errorConflict": "Sneltoets wordt al gebruikt door {{tool}}.",
"errorModifier": {
"mac": "Neem ⌘ (Command), ⌥ (Option) of een andere modificatietoets op in je sneltoets.",
"windows": "Neem Ctrl, Alt of een andere modificatietoets op in je sneltoets."
},
"searchPlaceholder": "Tools zoeken...",
"none": "Niet toegewezen",
"customBadge": "Aangepast",
@@ -853,7 +899,7 @@
"desc": "Bekijk en bewerk Stirling PDF JSON-exporten met gegroepeerde tekstbewerking en het opnieuw genereren van PDF's"
},
"addText": {
"tags": "text,annotation,label",
"tags": "tekst,annotatie,label",
"title": "Tekst toevoegen",
"desc": "Voeg overal in je PDF aangepaste tekst toe"
}
@@ -1125,7 +1171,7 @@
"settings": "Instellingen",
"conversionCompleted": "Conversie voltooid",
"results": "Resultaten",
"defaultFilename": "converted_file",
"defaultFilename": "geconverteerd_bestand",
"conversionResults": "Conversieresultaten",
"convertFrom": "Converteren van",
"convertTo": "Converteren naar",
@@ -2204,7 +2250,7 @@
"emptyDescription": "Teken, upload of typ hierboven een handtekening en kies vervolgens 'Opslaan in bibliotheek' om tot {{max}} favorieten paraat te hebben.",
"type": {
"canvas": "Tekening",
"image": "Upload",
"image": "Uploaden",
"text": "Tekst"
},
"limitTitle": "Limiet bereikt",
@@ -3289,7 +3335,7 @@
"tags": "splitsen, hoofdstukken, bookmarks, organiseren"
},
"validateSignature": {
"tags": "signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate",
"tags": "handtekening,verifiëren,valideren,pdf,certificaat,digitale handtekening,Handtekening valideren,Certificaat valideren",
"title": "PDF-handtekeningen valideren",
"header": "Digitale handtekeningen valideren",
"selectPDF": "Selecteer een ondertekend PDF-bestand",
@@ -3787,7 +3833,7 @@
"title": "API-documentatie",
"header": "API-documentatie",
"desc": "Bekijk en test de Stirling PDF API-endpoints",
"tags": "api,documentation,swagger,endpoints,development"
"tags": "api,documentatie,swagger,endpoints,ontwikkeling"
},
"cookieBanner": {
"popUp": {
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Beschikbare talen",
"description": "Talen waaruit gebruikers kunnen kiezen (laat leeg om alle talen in te schakelen)"
"description": "Talen waaruit gebruikers kunnen kiezen (laat leeg om alle talen in te schakelen)",
"placeholder": "Selecteer talen"
},
"customMetadata": {
"label": "Aangepaste metadata",
@@ -4061,7 +4108,9 @@
"label": "Logostijl",
"description": "Kies tussen het moderne minimalistische logo of het klassieke S-pictogram",
"classic": "Klassiek",
"modern": "Modern"
"classicAlt": "Klassiek logo",
"modern": "Modern",
"modernAlt": "Modern logo"
},
"customPaths": {
"label": "Aangepaste paden",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Applicatieconfiguratie",
"description": "Huidige applicatie-instellingen en configuratiedetails."
"description": "Huidige applicatie-instellingen en configuratiedetails.",
"loading": "Configuratie laden...",
"error": "Fout",
"warning": "Configuratiewaarschuwing",
"sections": {
"basic": "Basisconfiguratie",
"security": "Beveiligingsconfiguratie",
"system": "Systeemconfiguratie",
"integration": "Integratieconfiguratie"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Beheer teams en organiseer leden van de werkruimte",
"loading": "Teams laden...",
"loadingDetails": "Teamdetails laden...",
"loadError": "Teamdetails laden mislukt",
"createNewTeam": "Nieuw team maken",
"teamName": "Teamnaam",
"totalMembers": "Totaal aantal leden",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Bezig met afrekenproces voorbereiden...",
"redirecting": "Doorsturen naar beveiligde checkout...",
"upgradeTitle": "Upgraden naar {{planName}}",
"success": "Betaling geslaagd!",
"successMessage": "Uw abonnement is succesvol geactiveerd. U ontvangt binnenkort een bevestigingsmail.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferanser"
},
"workspace": {
"title": "Arbeidsområde",
"people": "Personer",
"teams": "Team"
},
"configuration": {
"title": "Konfigurasjon",
"systemSettings": "Systeminnstillinger",
"features": "Funksjoner",
"endpoints": "Endepunkter",
"database": "Database",
"advanced": "Avansert"
},
"securityAuth": {
"title": "Sikkerhet og autentisering",
"security": "Sikkerhet",
"connections": "Tilkoblinger"
},
"licensingAnalytics": {
"title": "Lisensiering og analyse",
"plan": "Plan",
"audit": "Revisjon",
"usageAnalytics": "Bruksanalyse"
},
"policiesPrivacy": {
"title": "Retningslinjer og personvern",
"legal": "Juridisk",
"privacy": "Personvern"
},
"developer": {
"title": "Utvikler",
"apiKeys": "API-nøkler"
},
"tooltips": {
"enableLoginFirst": "Aktiver påloggingsmodus først",
"requiresEnterprise": "Krever Enterprise-lisens"
},
"connection": {
"title": "Tilkoblingsmodus",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Fjern deaktiverte konverteringsvalg i Konverter-verktøyet i stedet for å vise dem nedtonet."
},
"hotkeys": {
"title": "Tastatursnarveier",
"description": "Tilpass tastatursnarveier for rask tilgang til verktøy. Klikk \"Endre snarvei\" og trykk en ny tastekombinasjon. Trykk Esc for å avbryte.",
"errorConflict": "Snarvei brukes allerede av {{tool}}.",
"errorModifier": {
"mac": "Inkluder ⌘ (Command), ⌥ (Option) eller en annen modifikator i snarveien.",
"windows": "Inkluder Ctrl, Alt eller en annen modifikator i snarveien."
},
"searchPlaceholder": "Søk i verktøy...",
"none": "Ikke tilordnet",
"customBadge": "Egendefinert",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Tilgjengelige språk",
"description": "Språk brukere kan velge mellom (la stå tomt for å aktivere alle språk)"
"description": "Språk brukere kan velge mellom (la stå tomt for å aktivere alle språk)",
"placeholder": "Velg språk"
},
"customMetadata": {
"label": "Egendefinert metadata",
@@ -4061,7 +4108,9 @@
"label": "Logostil",
"description": "Velg mellom den moderne minimalistiske logoen eller det klassiske S-ikonet",
"classic": "Klassisk",
"modern": "Moderne"
"classicAlt": "Klassisk logo",
"modern": "Moderne",
"modernAlt": "Moderne logo"
},
"customPaths": {
"label": "Egendefinerte stier",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Programkonfigurasjon",
"description": "Gjeldende programinnstillinger og konfigurasjonsdetaljer."
"description": "Gjeldende programinnstillinger og konfigurasjonsdetaljer.",
"loading": "Laster konfigurasjon...",
"error": "Feil",
"warning": "Konfigurasjonsadvarsel",
"sections": {
"basic": "Grunnleggende konfigurasjon",
"security": "Sikkerhetskonfigurasjon",
"system": "Systemkonfigurasjon",
"integration": "Integrasjonskonfigurasjon"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Administrer team og organiser medlemmer i arbeidsområdet",
"loading": "Laster team...",
"loadingDetails": "Laster teamdetaljer...",
"loadError": "Kunne ikke laste teamdetaljer",
"createNewTeam": "Opprett nytt team",
"teamName": "Teamnavn",
"totalMembers": "Totalt antall medlemmer",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Forbereder kassen...",
"redirecting": "Videresender til sikker betaling...",
"upgradeTitle": "Oppgrader til {{planName}}",
"success": "Betaling vellykket!",
"successMessage": "Abonnementet ditt er aktivert. Du vil snart motta en bekreftelses-e-post.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferencje"
},
"workspace": {
"title": "Przestrzeń robocza",
"people": "Osoby",
"teams": "Zespoły"
},
"configuration": {
"title": "Konfiguracja",
"systemSettings": "Ustawienia systemowe",
"features": "Funkcje",
"endpoints": "Endpointy",
"database": "Baza danych",
"advanced": "Zaawansowane"
},
"securityAuth": {
"title": "Bezpieczeństwo i uwierzytelnianie",
"security": "Bezpieczeństwo",
"connections": "Połączenia"
},
"licensingAnalytics": {
"title": "Licencje i analityka",
"plan": "Plan",
"audit": "Audyt",
"usageAnalytics": "Analityka użycia"
},
"policiesPrivacy": {
"title": "Zasady i prywatność",
"legal": "Prawne",
"privacy": "Prywatność"
},
"developer": {
"title": "Deweloper",
"apiKeys": "Klucze API"
},
"tooltips": {
"enableLoginFirst": "Najpierw włącz tryb logowania",
"requiresEnterprise": "Wymaga licencji Enterprise"
},
"connection": {
"title": "Tryb połączenia",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Usuń wyłączone opcje konwersji w narzędziu Konwertuj zamiast wyświetlać je wyszarzone."
},
"hotkeys": {
"title": "Skróty klawiaturowe",
"description": "Dostosuj skróty klawiaturowe, aby szybko uzyskiwać dostęp do narzędzi. Kliknij \"Zmień skrót\" i naciśnij nową kombinację klawiszy. Naciśnij Esc, aby anulować.",
"errorConflict": "Skrót już używany przez {{tool}}.",
"errorModifier": {
"mac": "Dodaj ⌘ (Command), ⌥ (Option) lub inny modyfikator do skrótu.",
"windows": "Dodaj Ctrl, Alt lub inny modyfikator do skrótu."
},
"searchPlaceholder": "Szukaj narzędzi...",
"none": "Nieprzypisany",
"customBadge": "Własny",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Dostępne języki",
"description": "Języki, które użytkownicy mogą wybierać (pozostaw puste, aby włączyć wszystkie języki)"
"description": "Języki, które użytkownicy mogą wybierać (pozostaw puste, aby włączyć wszystkie języki)",
"placeholder": "Wybierz języki"
},
"customMetadata": {
"label": "Niestandardowe metadane",
@@ -4061,7 +4108,9 @@
"label": "Styl logo",
"description": "Wybierz między nowoczesnym minimalistycznym logo a klasyczną ikoną S",
"classic": "Klasyczny",
"modern": "Nowoczesny"
"classicAlt": "Klasyczne logo",
"modern": "Nowoczesny",
"modernAlt": "Nowoczesne logo"
},
"customPaths": {
"label": "Niestandardowe ścieżki",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfiguracja aplikacji",
"description": "Bieżące ustawienia i szczegóły konfiguracji aplikacji."
"description": "Bieżące ustawienia i szczegóły konfiguracji aplikacji.",
"loading": "Ładowanie konfiguracji...",
"error": "Błąd",
"warning": "Ostrzeżenie konfiguracyjne",
"sections": {
"basic": "Podstawowa konfiguracja",
"security": "Konfiguracja zabezpieczeń",
"system": "Konfiguracja systemu",
"integration": "Konfiguracja integracji"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Zarządzaj zespołami i organizuj członków obszaru roboczego",
"loading": "Wczytywanie zespołów...",
"loadingDetails": "Wczytywanie szczegółów zespołu...",
"loadError": "Nie udało się wczytać szczegółów zespołu",
"createNewTeam": "Utwórz nowy zespół",
"teamName": "Nazwa zespołu",
"totalMembers": "Łącznie członków",
@@ -5431,7 +5490,7 @@
"from": "Od",
"perMonth": "/mies.",
"perSeat": "/miejsce",
"withServer": "+ Server Plan",
"withServer": "+ Plan serwerowy",
"licensedSeats": "Licencjonowane: {{count}} miejsc",
"includedInCurrent": "Wliczone w Twój plan",
"selectPlan": "Wybierz plan",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Przygotowywanie kasy...",
"redirecting": "Przekierowywanie do bezpiecznej kasy...",
"upgradeTitle": "Uaktualnij do {{planName}}",
"success": "Płatność zakończona sukcesem!",
"successMessage": "Twoja subskrypcja została pomyślnie aktywowana. Wkrótce otrzymasz email z potwierdzeniem.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferências"
},
"workspace": {
"title": "Espaço de trabalho",
"people": "Pessoas",
"teams": "Equipes"
},
"configuration": {
"title": "Configuração",
"systemSettings": "Configurações do sistema",
"features": "Recursos",
"endpoints": "Endpoints",
"database": "Banco de dados",
"advanced": "Avançado"
},
"securityAuth": {
"title": "Segurança & Autenticação",
"security": "Segurança",
"connections": "Conexões"
},
"licensingAnalytics": {
"title": "Licenciamento & Análises",
"plan": "Plano",
"audit": "Auditoria",
"usageAnalytics": "Análises de uso"
},
"policiesPrivacy": {
"title": "Políticas & Privacidade",
"legal": "Jurídico",
"privacy": "Privacidade"
},
"developer": {
"title": "Desenvolvedor",
"apiKeys": "Chaves de API"
},
"tooltips": {
"enableLoginFirst": "Ative o modo de login primeiro",
"requiresEnterprise": "Requer licença Enterprise"
},
"connection": {
"title": "Modo de conexão",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Remove opções de conversão desativadas na ferramenta Converter em vez de mostrá-las esmaecidas."
},
"hotkeys": {
"title": "Atalhos de teclado",
"description": "Personalize atalhos de teclado para acesso rápido às ferramentas. Clique em \"Alterar atalho\" e pressione uma nova combinação de teclas. Pressione Esc para cancelar.",
"errorConflict": "Atalho já usado por {{tool}}.",
"errorModifier": {
"mac": "Inclua ⌘ (Command), ⌥ (Option) ou outro modificador no atalho.",
"windows": "Inclua Ctrl, Alt ou outro modificador no atalho."
},
"searchPlaceholder": "Pesquisar ferramentas...",
"none": "Não atribuído",
"customBadge": "Personalizado",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Idiomas disponíveis",
"description": "Idiomas que os usuários podem selecionar (deixe em branco para habilitar todos os idiomas)"
"description": "Idiomas que os usuários podem selecionar (deixe em branco para habilitar todos os idiomas)",
"placeholder": "Selecione idiomas"
},
"customMetadata": {
"label": "Metadados personalizados",
@@ -4061,7 +4108,9 @@
"label": "Estilo do logo",
"description": "Escolha entre o logo minimalista moderno ou o ícone S clássico",
"classic": "Clássico",
"modern": "Moderno"
"classicAlt": "Logo clássico",
"modern": "Moderno",
"modernAlt": "Logo moderno"
},
"customPaths": {
"label": "Caminhos personalizados",
@@ -4281,7 +4330,7 @@
"description": "O nome do provedor SAML2"
},
"registrationId": {
"label": "Registration ID",
"label": "ID de registro",
"description": "O identificador de registro SAML2"
},
"autoCreateUser": {
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configuração do aplicativo",
"description": "Configurações e detalhes atuais do aplicativo."
"description": "Configurações e detalhes atuais do aplicativo.",
"loading": "Carregando configuração...",
"error": "Erro",
"warning": "Aviso de configuração",
"sections": {
"basic": "Configuração básica",
"security": "Configuração de segurança",
"system": "Configuração do sistema",
"integration": "Configuração de integração"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gerencie equipes e organize os membros do espaço de trabalho",
"loading": "Carregando equipes...",
"loadingDetails": "Carregando detalhes da equipe...",
"loadError": "Falha ao carregar detalhes da equipe",
"createNewTeam": "Criar nova equipe",
"teamName": "Nome da equipe",
"totalMembers": "Total de membros",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Preparando seu checkout...",
"redirecting": "Redirecionando para o checkout seguro...",
"upgradeTitle": "Fazer upgrade para {{planName}}",
"success": "Pagamento bem-sucedido!",
"successMessage": "Sua assinatura foi ativada com sucesso. Você receberá um e-mail de confirmação em breve.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferências"
},
"workspace": {
"title": "Espaço de trabalho",
"people": "Pessoas",
"teams": "Equipas"
},
"configuration": {
"title": "Configuração",
"systemSettings": "Definições do sistema",
"features": "Funcionalidades",
"endpoints": "Endpoints",
"database": "Base de dados",
"advanced": "Avançado"
},
"securityAuth": {
"title": "Segurança e Autenticação",
"security": "Segurança",
"connections": "Ligações"
},
"licensingAnalytics": {
"title": "Licenciamento e Análises",
"plan": "Plano",
"audit": "Auditoria",
"usageAnalytics": "Análise de utilização"
},
"policiesPrivacy": {
"title": "Políticas e Privacidade",
"legal": "Legal",
"privacy": "Privacidade"
},
"developer": {
"title": "Programador",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "Ative primeiro o modo de login",
"requiresEnterprise": "Requer licença Enterprise"
},
"connection": {
"title": "Modo de ligação",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Remover opções de conversão desativadas na ferramenta Converter em vez de as mostrar esbatidas."
},
"hotkeys": {
"title": "Atalhos de teclado",
"description": "Personalize os atalhos de teclado para acesso rápido às ferramentas. Clique \"Alterar atalho\" e prima uma nova combinação de teclas. Prima Esc para cancelar.",
"errorConflict": "Atalho já utilizado por {{tool}}.",
"errorModifier": {
"mac": "Inclua ⌘ (Command), ⌥ (Option) ou outro modificador no seu atalho.",
"windows": "Inclua Ctrl, Alt ou outro modificador no seu atalho."
},
"searchPlaceholder": "Procurar ferramentas...",
"none": "Não atribuído",
"customBadge": "Personalizado",
@@ -3821,7 +3867,7 @@
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off."
},
"analytics": {
"title": "Analytics",
"title": "Análises",
"description": "Estes cookies ajudam-nos a compreender como as nossas ferramentas estão a ser usadas, para que possamos focar-nos em construir as funcionalidades que a nossa comunidade mais valoriza. Fique descansado — o Stirling PDF não pode e nunca irá rastrear o conteúdo dos documentos com que trabalha."
}
},
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Idiomas disponíveis",
"description": "Idiomas que os utilizadores podem selecionar (deixe vazio para ativar todos os idiomas)"
"description": "Idiomas que os utilizadores podem selecionar (deixe vazio para ativar todos os idiomas)",
"placeholder": "Selecionar idiomas"
},
"customMetadata": {
"label": "Metadados personalizados",
@@ -4061,7 +4108,9 @@
"label": "Estilo do logótipo",
"description": "Escolha entre o logótipo minimalista moderno ou o ícone S clássico",
"classic": "Clássico",
"modern": "Moderno"
"classicAlt": "Logótipo clássico",
"modern": "Moderno",
"modernAlt": "Logótipo moderno"
},
"customPaths": {
"label": "Caminhos personalizados",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configuração da aplicação",
"description": "Definições atuais da aplicação e detalhes de configuração."
"description": "Definições atuais da aplicação e detalhes de configuração.",
"loading": "A carregar configuração...",
"error": "Erro",
"warning": "Aviso de configuração",
"sections": {
"basic": "Configuração básica",
"security": "Configuração de segurança",
"system": "Configuração do sistema",
"integration": "Configuração de integração"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gerir equipas e organizar membros do espaço de trabalho",
"loading": "A carregar equipas...",
"loadingDetails": "A carregar detalhes da equipa...",
"loadError": "Falha ao carregar detalhes da equipa",
"createNewTeam": "Criar nova equipa",
"teamName": "Nome da equipa",
"totalMembers": "Total de membros",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "A preparar o pagamento...",
"redirecting": "A redirecionar para o checkout seguro...",
"upgradeTitle": "Atualizar para {{planName}}",
"success": "Pagamento bem-sucedido!",
"successMessage": "A sua subscrição foi ativada com sucesso. Irá receber um email de confirmação em breve.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferințe"
},
"workspace": {
"title": "Spațiu de lucru",
"people": "Persoane",
"teams": "Echipe"
},
"configuration": {
"title": "Configurație",
"systemSettings": "Setări sistem",
"features": "Funcții",
"endpoints": "Endpoint-uri",
"database": "Bază de date",
"advanced": "Avansat"
},
"securityAuth": {
"title": "Securitate și autentificare",
"security": "Securitate",
"connections": "Conexiuni"
},
"licensingAnalytics": {
"title": "Licențiere și Analytics",
"plan": "Plan",
"audit": "Audit",
"usageAnalytics": "Analytics de utilizare"
},
"policiesPrivacy": {
"title": "Politici și confidențialitate",
"legal": "Legal",
"privacy": "Confidențialitate"
},
"developer": {
"title": "Dezvoltator",
"apiKeys": "Chei API"
},
"tooltips": {
"enableLoginFirst": "Activați mai întâi modul de login",
"requiresEnterprise": "Necesită licență Enterprise"
},
"connection": {
"title": "Mod de conectare",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Elimină opțiunile de conversie dezactivate în instrumentul Convert, în loc să fie afișate estompate."
},
"hotkeys": {
"title": "Scurtături de tastatură",
"description": "Personalizați scurtăturile de tastatură pentru acces rapid la instrumente. Faceți clic pe \"Schimbă scurtătura\" și apăsați o nouă combinație de taste. Apăsați Esc pentru a anula.",
"errorConflict": "Scurtătura este deja utilizată de {{tool}}.",
"errorModifier": {
"mac": "Includeți ⌘ (Command), ⌥ (Option) sau un alt modificator în scurtătură.",
"windows": "Includeți Ctrl, Alt sau un alt modificator în scurtătură."
},
"searchPlaceholder": "Căutați instrumente...",
"none": "Neatribuit",
"customBadge": "Personalizat",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Limbi disponibile",
"description": "Limbile din care utilizatorii pot alege (lăsați gol pentru a activa toate limbile)"
"description": "Limbile din care utilizatorii pot alege (lăsați gol pentru a activa toate limbile)",
"placeholder": "Selectați limbi"
},
"customMetadata": {
"label": "Metadate personalizate",
@@ -4061,7 +4108,9 @@
"label": "Stil logo",
"description": "Alegeți între logo-ul minimalist modern sau pictograma clasică S",
"classic": "Clasic",
"modern": "Modern"
"classicAlt": "Logo clasic",
"modern": "Modern",
"modernAlt": "Logo modern"
},
"customPaths": {
"label": "Căi personalizate",
@@ -4427,7 +4476,7 @@
"description": "Timp maxim de execuție"
},
"libreOffice": "LibreOffice",
"pdfToHtml": "PDF to HTML",
"pdfToHtml": "PDF în HTML",
"qpdf": "QPDF",
"tesseract": "Tesseract OCR",
"pythonOpenCv": "Python OpenCV",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Configurarea aplicației",
"description": "Setările curente ale aplicației și detalii de configurare."
"description": "Setările curente ale aplicației și detalii de configurare.",
"loading": "Se încarcă configurația...",
"error": "Eroare",
"warning": "Avertisment configurație",
"sections": {
"basic": "Configurație de bază",
"security": "Configurație de securitate",
"system": "Configurație sistem",
"integration": "Configurație integrare"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Gestionați echipele și organizați membrii spațiului de lucru",
"loading": "Se încarcă echipele...",
"loadingDetails": "Se încarcă detaliile echipei...",
"loadError": "Încărcarea detaliilor echipei a eșuat",
"createNewTeam": "Creați echipă nouă",
"teamName": "Numele echipei",
"totalMembers": "Total membri",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Se pregătește checkout-ul...",
"redirecting": "Se redirecționează către checkout securizat...",
"upgradeTitle": "Treci la {{planName}}",
"success": "Plată reușită!",
"successMessage": "Abonamentul dvs. a fost activat cu succes. Veți primi în curând un e-mail de confirmare.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Настройки"
},
"workspace": {
"title": "Рабочее пространство",
"people": "Участники",
"teams": "Команды"
},
"configuration": {
"title": "Конфигурация",
"systemSettings": "Системные настройки",
"features": "Функции",
"endpoints": "Конечные точки",
"database": "База данных",
"advanced": "Дополнительно"
},
"securityAuth": {
"title": "Безопасность и аутентификация",
"security": "Безопасность",
"connections": "Подключения"
},
"licensingAnalytics": {
"title": "Лицензирование и аналитика",
"plan": "План",
"audit": "Аудит",
"usageAnalytics": "Аналитика использования"
},
"policiesPrivacy": {
"title": "Политики и конфиденциальность",
"legal": "Юридическая информация",
"privacy": "Конфиденциальность"
},
"developer": {
"title": "Разработчик",
"apiKeys": "Ключи API"
},
"tooltips": {
"enableLoginFirst": "Сначала включите режим входа",
"requiresEnterprise": "Требуется лицензия Enterprise"
},
"connection": {
"title": "Режим подключения",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Убирать отключённые параметры конвертации в инструменте Convert вместо отображения их серыми."
},
"hotkeys": {
"title": "Горячие клавиши",
"description": "Настройте горячие клавиши для быстрого доступа к инструментам. Нажмите «Изменить сочетание» и введите новую комбинацию клавиш. Нажмите Esc, чтобы отменить.",
"errorConflict": "Сочетание уже используется инструментом {{tool}}.",
"errorModifier": {
"mac": "Добавьте ⌘ (Command), ⌥ (Option) или другой модификатор в сочетание.",
"windows": "Добавьте Ctrl, Alt или другой модификатор в сочетание."
},
"searchPlaceholder": "Поиск инструментов...",
"none": "Не назначено",
"customBadge": "Пользовательское",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Доступные языки",
"description": "Языки, которые пользователи могут выбрать (оставьте пустым, чтобы включить все языки)"
"description": "Языки, которые пользователи могут выбрать (оставьте пустым, чтобы включить все языки)",
"placeholder": "Выберите языки"
},
"customMetadata": {
"label": "Пользовательские метаданные",
@@ -4061,7 +4108,9 @@
"label": "Стиль логотипа",
"description": "Выберите между современным минималистичным логотипом и классической иконкой S",
"classic": "Классический",
"modern": "Современный"
"classicAlt": "Классический логотип",
"modern": "Современный",
"modernAlt": "Современный логотип"
},
"customPaths": {
"label": "Пользовательские пути",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Конфигурация приложения",
"description": "Текущие настройки и подробности конфигурации приложения."
"description": "Текущие настройки и подробности конфигурации приложения.",
"loading": "Загрузка конфигурации...",
"error": "Ошибка",
"warning": "Предупреждение конфигурации",
"sections": {
"basic": "Базовая конфигурация",
"security": "Конфигурация безопасности",
"system": "Системная конфигурация",
"integration": "Конфигурация интеграций"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Управляйте командами и организуйте участников рабочего пространства",
"loading": "Загрузка команд...",
"loadingDetails": "Загрузка сведений о команде...",
"loadError": "Не удалось загрузить сведения о команде",
"createNewTeam": "Создать новую команду",
"teamName": "Название команды",
"totalMembers": "Всего участников",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Подготовка к оплате...",
"redirecting": "Перенаправление на безопасную оплату...",
"upgradeTitle": "Обновить до {{planName}}",
"success": "Платеж успешно выполнен!",
"successMessage": "Ваша подписка успешно активирована. Вскоре вы получите письмо с подтверждением.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Predvoľby"
},
"workspace": {
"title": "Pracovný priestor",
"people": "Ľudia",
"teams": "Tímy"
},
"configuration": {
"title": "Konfigurácia",
"systemSettings": "Systémové nastavenia",
"features": "Funkcie",
"endpoints": "Endpointy",
"database": "Databáza",
"advanced": "Pokročilé"
},
"securityAuth": {
"title": "Zabezpečenie a autentifikácia",
"security": "Zabezpečenie",
"connections": "Pripojenia"
},
"licensingAnalytics": {
"title": "Licencovanie a analytika",
"plan": "Plán",
"audit": "Audit",
"usageAnalytics": "Analytika používania"
},
"policiesPrivacy": {
"title": "Zásady a súkromie",
"legal": "Právne",
"privacy": "Súkromie"
},
"developer": {
"title": "Vývojár",
"apiKeys": "API kľúče"
},
"tooltips": {
"enableLoginFirst": "Najprv zapnite režim prihlásenia",
"requiresEnterprise": "Vyžaduje licenciu Enterprise"
},
"connection": {
"title": "Režim pripojenia",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Odstrániť zakázané možnosti konverzie v nástroji Konvertovať namiesto ich sivého zobrazenia."
},
"hotkeys": {
"title": "Klávesové skratky",
"description": "Prispôsobte klávesové skratky pre rýchly prístup k nástrojom. Kliknite na \"Zmeniť skratku\" a stlačte novú kombináciu klávesov. Stlačením Esc zrušíte.",
"errorConflict": "Skratka sa už používa nástrojom {{tool}}.",
"errorModifier": {
"mac": "Do skratky zahrňte ⌘ (Command), ⌥ (Option) alebo iný modifikátor.",
"windows": "Do skratky zahrňte Ctrl, Alt alebo iný modifikátor."
},
"searchPlaceholder": "Hľadať nástroje...",
"none": "Nepriradené",
"customBadge": "Vlastná",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Dostupné jazyky",
"description": "Jazyky, ktoré si môžu používatelia vybrať (ponechajte prázdne pre povolenie všetkých jazykov)"
"description": "Jazyky, ktoré si môžu používatelia vybrať (ponechajte prázdne pre povolenie všetkých jazykov)",
"placeholder": "Vyberte jazyky"
},
"customMetadata": {
"label": "Vlastné metadáta",
@@ -4061,7 +4108,9 @@
"label": "Štýl loga",
"description": "Vyberte si medzi moderným minimalistickým logom alebo klasickou ikonou S",
"classic": "Klasické",
"modern": "Moderné"
"classicAlt": "Klasické logo",
"modern": "Moderné",
"modernAlt": "Moderné logo"
},
"customPaths": {
"label": "Vlastné cesty",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfigurácia aplikácie",
"description": "Aktuálne nastavenia aplikácie a podrobnosti konfigurácie."
"description": "Aktuálne nastavenia aplikácie a podrobnosti konfigurácie.",
"loading": "Načítava sa konfigurácia...",
"error": "Chyba",
"warning": "Varovanie konfigurácie",
"sections": {
"basic": "Základná konfigurácia",
"security": "Bezpečnostná konfigurácia",
"system": "Systémová konfigurácia",
"integration": "Integračná konfigurácia"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Spravujte tímy a organizujte členov pracovného priestoru",
"loading": "Načítavajú sa tímy...",
"loadingDetails": "Načítavajú sa podrobnosti tímu...",
"loadError": "Nepodarilo sa načítať podrobnosti tímu",
"createNewTeam": "Vytvoriť nový tím",
"teamName": "Názov tímu",
"totalMembers": "Spolu členov",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Pripravuje sa platba...",
"redirecting": "Presmerovanie na zabezpečenú pokladňu...",
"upgradeTitle": "Inovovať na {{planName}}",
"success": "Platba bola úspešná!",
"successMessage": "Vaše predplatné bolo úspešne aktivované. Čoskoro dostanete potvrdzujúci e-mail.",
+66 -6
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Nastavitve"
},
"workspace": {
"title": "Delovni prostor",
"people": "Osebe",
"teams": "Skupine"
},
"configuration": {
"title": "Konfiguracija",
"systemSettings": "Sistemske nastavitve",
"features": "Funkcije",
"endpoints": "Končne točke",
"database": "Podatkovna baza",
"advanced": "Napredno"
},
"securityAuth": {
"title": "Varnost in avtentikacija",
"security": "Varnost",
"connections": "Povezave"
},
"licensingAnalytics": {
"title": "Licenciranje in analitika",
"plan": "Načrt",
"audit": "Revizija",
"usageAnalytics": "Analitika uporabe"
},
"policiesPrivacy": {
"title": "Politike in zasebnost",
"legal": "Pravno",
"privacy": "Zasebnost"
},
"developer": {
"title": "Razvijalec",
"apiKeys": "API ključi"
},
"tooltips": {
"enableLoginFirst": "Najprej omogočite način prijave",
"requiresEnterprise": "Zahteva licenco Enterprise"
},
"connection": {
"title": "Način povezave",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Odstrani onemogočene možnosti pretvorbe v orodju Pretvori, namesto da so zasivljene."
},
"hotkeys": {
"title": "Bližnjice na tipkovnici",
"description": "Prilagodite bližnjice na tipkovnici za hiter dostop do orodij. Kliknite \"Spremeni bližnjico\" in pritisnite novo kombinacijo tipk. Pritisnite Esc za preklic.",
"errorConflict": "Bližnjica je že v uporabi pri {{tool}}.",
"errorModifier": {
"mac": "Vključite ⌘ (Command), ⌥ (Option) ali drug modifikator v bližnjici.",
"windows": "Vključite Ctrl, Alt ali drug modifikator v bližnjici."
},
"searchPlaceholder": "Išči orodja...",
"none": "Ni določeno",
"customBadge": "Po meri",
@@ -1165,11 +1211,11 @@
"fileFormat": "Format datoteke",
"wordDoc": "Dokument Word",
"wordDocExt": "Dokument Word (.docx)",
"odtExt": "OpenDocument Text (.odt)",
"odtExt": "Besedilo OpenDocument (.odt)",
"pptExt": "PowerPoint (.pptx)",
"odpExt": "OpenDocument Presentation (.odp)",
"odpExt": "Predstavitev OpenDocument (.odp)",
"txtExt": "Navadno besedilo (.txt)",
"rtfExt": "Rich Text Format (.rtf)",
"rtfExt": "Obogateno besedilo (.rtf)",
"selectedFiles": "Izbrane datoteke",
"noFileSelected": "Ni izbrane datoteke. Uporabite ploščo z datotekami za dodajanje.",
"convertFiles": "Pretvori datoteke",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Razpoložljivi jeziki",
"description": "Jeziki, med katerimi lahko uporabniki izbirajo (pustite prazno za omogočanje vseh jezikov)"
"description": "Jeziki, med katerimi lahko uporabniki izbirajo (pustite prazno za omogočanje vseh jezikov)",
"placeholder": "Izberite jezike"
},
"customMetadata": {
"label": "Metapodatki po meri",
@@ -4061,7 +4108,9 @@
"label": "Slog logotipa",
"description": "Izberite med modernim minimalističnim logotipom ali klasično ikono S",
"classic": "Klasičen",
"modern": "Sodoben"
"classicAlt": "Klasični logotip",
"modern": "Sodoben",
"modernAlt": "Moderni logotip"
},
"customPaths": {
"label": "Poti po meri",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfiguracija aplikacije",
"description": "Trenutne nastavitve aplikacije in podrobnosti konfiguracije."
"description": "Trenutne nastavitve aplikacije in podrobnosti konfiguracije.",
"loading": "Nalaganje konfiguracije...",
"error": "Napaka",
"warning": "Opozorilo konfiguracije",
"sections": {
"basic": "Osnovna konfiguracija",
"security": "Varnostna konfiguracija",
"system": "Sistemska konfiguracija",
"integration": "Konfiguracija integracije"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Upravljajte skupine in organizirajte člane delovnega prostora",
"loading": "Nalaganje skupin...",
"loadingDetails": "Nalaganje podrobnosti skupine...",
"loadError": "Nalaganje podrobnosti skupine ni uspelo",
"createNewTeam": "Ustvari novo skupino",
"teamName": "Ime skupine",
"totalMembers": "Skupaj članov",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Pripravljamo vaš nakup...",
"redirecting": "Preusmerjanje na varno blagajno...",
"upgradeTitle": "Nadgradite na {{planName}}",
"success": "Plačilo uspešno!",
"successMessage": "Vaša naročnina je bila uspešno aktivirana. Kmalu boste prejeli potrditveno e-pošto.",
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferencije"
},
"workspace": {
"title": "Radni prostor",
"people": "Osobe",
"teams": "Timovi"
},
"configuration": {
"title": "Konfiguracija",
"systemSettings": "Sistemska podešavanja",
"features": "Funkcije",
"endpoints": "Krajnje tačke",
"database": "Baza podataka",
"advanced": "Napredno"
},
"securityAuth": {
"title": "Bezbednost i autentikacija",
"security": "Bezbednost",
"connections": "Konekcije"
},
"licensingAnalytics": {
"title": "Licenciranje i analitika",
"plan": "Plan",
"audit": "Revizija",
"usageAnalytics": "Analitika korišćenja"
},
"policiesPrivacy": {
"title": "Politike i privatnost",
"legal": "Pravno",
"privacy": "Privatnost"
},
"developer": {
"title": "Razvoj",
"apiKeys": "API ključevi"
},
"tooltips": {
"enableLoginFirst": "Prvo uključite režim prijave",
"requiresEnterprise": "Zahteva Enterprise licencu"
},
"connection": {
"title": "Režim povezivanja",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Uklonite onemogućene opcije konverzije u alatu Convert umesto da budu zasivljene."
},
"hotkeys": {
"title": "Prečice na tastaturi",
"description": "Prilagodite prečice na tastaturi za brz pristup alatima. Kliknite \"Promeni prečicu\" i pritisnite novu kombinaciju tastera. Pritisnite Esc za otkazivanje.",
"errorConflict": "Prečicu već koristi {{tool}}.",
"errorModifier": {
"mac": "Uključite ⌘ (Command), ⌥ (Option) ili neki drugi modifikator u prečicu.",
"windows": "Uključite Ctrl, Alt ili neki drugi modifikator u prečicu."
},
"searchPlaceholder": "Pretraži alate...",
"none": "Nije dodeljeno",
"customBadge": "Prilagođeno",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Dostupni jezici",
"description": "Jezici koje korisnici mogu izabrati (ostavite prazno da omogućite sve jezike)"
"description": "Jezici koje korisnici mogu izabrati (ostavite prazno da omogućite sve jezike)",
"placeholder": "Izaberite jezike"
},
"customMetadata": {
"label": "Prilagođeni metapodaci",
@@ -4061,7 +4108,9 @@
"label": "Stil logotipa",
"description": "Izaberite između modernog minimalističkog logotipa ili klasične S ikone",
"classic": "Klasični",
"modern": "Moderni"
"classicAlt": "Klasični logo",
"modern": "Moderni",
"modernAlt": "Moderni logo"
},
"customPaths": {
"label": "Prilagođene putanje",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Konfiguracija aplikacije",
"description": "Trenutna podešavanja aplikacije i detalji konfiguracije."
"description": "Trenutna podešavanja aplikacije i detalji konfiguracije.",
"loading": "Učitavanje konfiguracije...",
"error": "Greška",
"warning": "Upozorenje o konfiguraciji",
"sections": {
"basic": "Osnovna konfiguracija",
"security": "Bezbednosna konfiguracija",
"system": "Sistemska konfiguracija",
"integration": "Konfiguracija integracije"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Upravljajte timovima i organizujte članove radnog prostora",
"loading": "Učitavanje timova...",
"loadingDetails": "Učitavanje detalja tima...",
"loadError": "Učitavanje detalja tima nije uspelo",
"createNewTeam": "Kreiraj novi tim",
"teamName": "Naziv tima",
"totalMembers": "Ukupno članova",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Priprema vaše kupovine...",
"redirecting": "Preusmeravanje na sigurnu naplatu...",
"upgradeTitle": "Nadogradi na {{planName}}",
"success": "Plaćanje uspešno!",
"successMessage": "Vaša pretplata je uspešno aktivirana. Uskoro ćete dobiti potvrdni email.",
@@ -5749,7 +5809,7 @@
},
"table": {
"title": "Detaljna statistika",
"endpoint": "Endpoint",
"endpoint": "Krajnja tačka",
"visits": "Posete",
"percentage": "Procenat",
"noData": "Nema dostupnih podataka"
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Preferenser"
},
"workspace": {
"title": "Arbetsyta",
"people": "Personer",
"teams": "Team"
},
"configuration": {
"title": "Konfiguration",
"systemSettings": "Systeminställningar",
"features": "Funktioner",
"endpoints": "Slutpunkter",
"database": "Databas",
"advanced": "Avancerat"
},
"securityAuth": {
"title": "Säkerhet och autentisering",
"security": "Säkerhet",
"connections": "Anslutningar"
},
"licensingAnalytics": {
"title": "Licensiering och analys",
"plan": "Plan",
"audit": "Granskning",
"usageAnalytics": "Användningsanalys"
},
"policiesPrivacy": {
"title": "Policyer och integritet",
"legal": "Juridik",
"privacy": "Integritet"
},
"developer": {
"title": "Utvecklare",
"apiKeys": "API-nycklar"
},
"tooltips": {
"enableLoginFirst": "Aktivera inloggningsläge först",
"requiresEnterprise": "Kräver Enterprise-licens"
},
"connection": {
"title": "Anslutningsläge",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Ta bort inaktiverade konverteringsalternativ i verktyget Konvertera istället för att visa dem nedtonade."
},
"hotkeys": {
"title": "Kortkommandon",
"description": "Anpassa kortkommandon för snabb åtkomst till verktyg. Klicka på \"Ändra genväg\" och tryck en ny tangentkombination. Tryck Esc för att avbryta.",
"errorConflict": "Genväg används redan av {{tool}}.",
"errorModifier": {
"mac": "Inkludera ⌘ (Command), ⌥ (Option) eller en annan modifierare i ditt kortkommando.",
"windows": "Inkludera Ctrl, Alt eller en annan modifierare i ditt kortkommando."
},
"searchPlaceholder": "Sök bland verktyg...",
"none": "Inte tilldelad",
"customBadge": "Anpassad",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Tillgängliga språk",
"description": "Språk som användare kan välja mellan (lämna tomt för att aktivera alla språk)"
"description": "Språk som användare kan välja mellan (lämna tomt för att aktivera alla språk)",
"placeholder": "Välj språk"
},
"customMetadata": {
"label": "Anpassade metadata",
@@ -4061,7 +4108,9 @@
"label": "Logotypstil",
"description": "Välj mellan den moderna minimalistiska logotypen eller den klassiska S-ikonen",
"classic": "Klassisk",
"modern": "Modern"
"classicAlt": "Klassisk logotyp",
"modern": "Modern",
"modernAlt": "Modern logotyp"
},
"customPaths": {
"label": "Anpassade sökvägar",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Programkonfiguration",
"description": "Aktuella programinställningar och konfigurationsdetaljer."
"description": "Aktuella programinställningar och konfigurationsdetaljer.",
"loading": "Läser in konfiguration...",
"error": "Fel",
"warning": "Konfigurationsvarning",
"sections": {
"basic": "Grundläggande konfiguration",
"security": "Säkerhetskonfiguration",
"system": "Systemkonfiguration",
"integration": "Integrationskonfiguration"
}
},
"account": {
"overview": {
@@ -5087,7 +5145,7 @@
"replace-color": {
"options": {
"fill": "Fyllningsfärg",
"gradient": "Gradient"
"gradient": "Övertoning"
},
"previewOverlayOpacity": "Opacitet för förhandsöverlägg",
"previewOverlayTransparency": "Transparens för förhandsöverlägg",
@@ -5342,6 +5400,7 @@
"description": "Hantera team och organisera arbetsytans medlemmar",
"loading": "Laddar team...",
"loadingDetails": "Laddar teamdetaljer...",
"loadError": "Det gick inte att läsa in teamets uppgifter",
"createNewTeam": "Skapa nytt team",
"teamName": "Teamnamn",
"totalMembers": "Totalt antal medlemmar",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Förbereder din kassa...",
"redirecting": "Omdirigerar till säker kassa...",
"upgradeTitle": "Uppgradera till {{planName}}",
"success": "Betalning genomförd!",
"successMessage": "Din prenumeration har aktiverats. Du får snart en bekräftelse via e-post.",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "ค่ากำหนด"
},
"workspace": {
"title": "พื้นที่ทำงาน",
"people": "สมาชิก",
"teams": "ทีม"
},
"configuration": {
"title": "การกำหนดค่า",
"systemSettings": "การตั้งค่าระบบ",
"features": "ฟีเจอร์",
"endpoints": "Endpoints",
"database": "ฐานข้อมูล",
"advanced": "ขั้นสูง"
},
"securityAuth": {
"title": "ความปลอดภัยและการยืนยันตัวตน",
"security": "ความปลอดภัย",
"connections": "การเชื่อมต่อ"
},
"licensingAnalytics": {
"title": "สิทธิ์การใช้งานและการวิเคราะห์",
"plan": "แผน",
"audit": "การตรวจสอบ",
"usageAnalytics": "การวิเคราะห์การใช้งาน"
},
"policiesPrivacy": {
"title": "นโยบายและความเป็นส่วนตัว",
"legal": "กฎหมาย",
"privacy": "ความเป็นส่วนตัว"
},
"developer": {
"title": "นักพัฒนา",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "เปิดใช้งานโหมดเข้าสู่ระบบก่อน",
"requiresEnterprise": "ต้องมีสิทธิ์การใช้งานระดับ Enterprise"
},
"connection": {
"title": "โหมดการเชื่อมต่อ",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "ลบตัวเลือกการแปลงที่ถูกปิดใช้งานในเครื่องมือ Convert แทนการแสดงเป็นจาง"
},
"hotkeys": {
"title": "แป้นพิมพ์ลัด",
"description": "ปรับแต่งแป้นพิมพ์ลัดเพื่อเข้าถึงเครื่องมือได้รวดเร็ว คลิก \"Change shortcut\" แล้วกดปุ่มลัดชุดใหม่ กด Esc เพื่อยกเลิก",
"errorConflict": "ปุ่มลัดถูกใช้โดย {{tool}} แล้ว",
"errorModifier": {
"mac": "ให้มี ⌘ (Command), ⌥ (Option) หรือปุ่มดัดแปลงอื่นในชุดแป้นลัดของคุณ",
"windows": "ให้มี Ctrl, Alt หรือปุ่มดัดแปลงอื่นในชุดแป้นลัดของคุณ"
},
"searchPlaceholder": "ค้นหาเครื่องมือ...",
"none": "ไม่ได้กำหนด",
"customBadge": "กำหนดเอง",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "ภาษาที่มีให้ใช้",
"description": "ภาษาที่ผู้ใช้สามารถเลือกได้ (ปล่อยว่างเพื่อเปิดใช้ทุกภาษา)"
"description": "ภาษาที่ผู้ใช้สามารถเลือกได้ (ปล่อยว่างเพื่อเปิดใช้ทุกภาษา)",
"placeholder": "เลือกภาษา"
},
"customMetadata": {
"label": "ข้อมูลเมตาแบบกำหนดเอง",
@@ -4061,7 +4108,9 @@
"label": "สไตล์โลโก้",
"description": "เลือกระหว่างโลโก้มินิมอลสมัยใหม่หรือไอคอน S แบบคลาสสิก",
"classic": "คลาสสิก",
"modern": "ทันสมัย"
"classicAlt": "โลโก้แบบคลาสสิก",
"modern": "ทันสมัย",
"modernAlt": "โลโก้แบบสมัยใหม่"
},
"customPaths": {
"label": "พาธแบบกำหนดเอง",
@@ -4303,7 +4352,7 @@
"description": "ใช้การกำหนดค่าฐานข้อมูลของคุณเองแทนฐานข้อมูลฝังตัวเริ่มต้น"
},
"customUrl": {
"label": "Custom Database URL",
"label": "URL ฐานข้อมูลแบบกำหนดเอง",
"description": "สตริงการเชื่อมต่อ JDBC แบบเต็ม (เช่น jdbc:postgresql://localhost:5432/postgres) หากระบุแล้ว การตั้งค่าการเชื่อมต่อรายข้อด้านล่างจะไม่ถูกใช้"
},
"type": {
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "การกำหนดค่าแอปพลิเคชัน",
"description": "การตั้งค่าและรายละเอียดการกำหนดค่าปัจจุบันของแอปพลิเคชัน"
"description": "การตั้งค่าและรายละเอียดการกำหนดค่าปัจจุบันของแอปพลิเคชัน",
"loading": "กำลังโหลดการกำหนดค่า...",
"error": "ข้อผิดพลาด",
"warning": "คำเตือนการกำหนดค่า",
"sections": {
"basic": "การกำหนดค่าพื้นฐาน",
"security": "การกำหนดค่าความปลอดภัย",
"system": "การกำหนดค่าระบบ",
"integration": "การกำหนดค่าการผสานรวม"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "จัดการทีมและจัดระเบียบสมาชิกในพื้นที่ทำงาน",
"loading": "กำลังโหลดทีม...",
"loadingDetails": "กำลังโหลดรายละเอียดทีม...",
"loadError": "ไม่สามารถโหลดรายละเอียดทีม",
"createNewTeam": "สร้างทีมใหม่",
"teamName": "ชื่อทีม",
"totalMembers": "สมาชิกทั้งหมด",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "กำลังเตรียมหน้าชำระเงินของคุณ...",
"redirecting": "กำลังเปลี่ยนเส้นทางไปยังการชำระเงินที่ปลอดภัย...",
"upgradeTitle": "อัปเกรดเป็น {{planName}}",
"success": "ชำระเงินสำเร็จ!",
"successMessage": "การสมัครสมาชิกของคุณเปิดใช้งานเรียบร้อยแล้ว คุณจะได้รับอีเมลยืนยันในไม่ช้า",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Tercihler"
},
"workspace": {
"title": "Çalışma Alanı",
"people": "Kişiler",
"teams": "Ekipler"
},
"configuration": {
"title": "Yapılandırma",
"systemSettings": "Sistem Ayarları",
"features": "Özellikler",
"endpoints": "Uç Noktalar",
"database": "Veritabanı",
"advanced": "Gelişmiş"
},
"securityAuth": {
"title": "Güvenlik ve Kimlik Doğrulama",
"security": "Güvenlik",
"connections": "Bağlantılar"
},
"licensingAnalytics": {
"title": "Lisanslama ve Analitik",
"plan": "Plan",
"audit": "Denetim",
"usageAnalytics": "Kullanım Analitiği"
},
"policiesPrivacy": {
"title": "Politikalar ve Gizlilik",
"legal": "Yasal",
"privacy": "Gizlilik"
},
"developer": {
"title": "Geliştirici",
"apiKeys": "API Anahtarları"
},
"tooltips": {
"enableLoginFirst": "Önce oturum açma modunu etkinleştirin",
"requiresEnterprise": "Enterprise lisansı gerektirir"
},
"connection": {
"title": "Bağlantı Modu",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Gri gösterilmek yerine Dönüştür aracındaki devre dışı seçenekleri kaldırın."
},
"hotkeys": {
"title": "Klavye Kısayolları",
"description": "Araçlara hızlı erişim için klavye kısayollarını özelleştirin. \"Kısayolu değiştir\"e tıklayın ve yeni bir tuş kombinasyonuna basın. İptal etmek için Esc'ye basın.",
"errorConflict": "Kısayol zaten {{tool}} tarafından kullanılıyor.",
"errorModifier": {
"mac": "Kısayolunuza ⌘ (Command), ⌥ (Option) veya başka bir değiştirici ekleyin.",
"windows": "Kısayolunuza Ctrl, Alt veya başka bir değiştirici ekleyin."
},
"searchPlaceholder": "Araçlarda ara...",
"none": "Atanmadı",
"customBadge": "Özel",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Kullanılabilir Diller",
"description": "Kullanıcıların seçebileceği diller (tüm dilleri etkinleştirmek için boş bırakın)"
"description": "Kullanıcıların seçebileceği diller (tüm dilleri etkinleştirmek için boş bırakın)",
"placeholder": "Dilleri seçin"
},
"customMetadata": {
"label": "Özel Üstveri",
@@ -4061,7 +4108,9 @@
"label": "Logo Stili",
"description": "Modern minimalist logo ile klasik S simgesi arasında seçim yapın",
"classic": "Klasik",
"modern": "Modern"
"classicAlt": "Klasik logo",
"modern": "Modern",
"modernAlt": "Modern logo"
},
"customPaths": {
"label": "Özel Yollar",
@@ -4499,7 +4548,7 @@
"description": "Çerez politikası için URL veya dosya adı"
},
"impressum": {
"label": "Impressum",
"label": "Künye",
"description": "Impressum için URL veya dosya adı (bazı yargı bölgelerinde gereklidir)"
}
},
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Uygulama Yapılandırması",
"description": "Geçerli uygulama ayarları ve yapılandırma ayrıntıları."
"description": "Geçerli uygulama ayarları ve yapılandırma ayrıntıları.",
"loading": "Yapılandırma yükleniyor...",
"error": "Hata",
"warning": "Yapılandırma Uyarısı",
"sections": {
"basic": "Temel Yapılandırma",
"security": "Güvenlik Yapılandırması",
"system": "Sistem Yapılandırması",
"integration": "Entegrasyon Yapılandırması"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Takımları yönetin ve çalışma alanı üyelerini organize edin",
"loading": "Takımlar yükleniyor...",
"loadingDetails": "Takım ayrıntıları yükleniyor...",
"loadError": "Ekip ayrıntıları yüklenemedi",
"createNewTeam": "Yeni Takım Oluştur",
"teamName": "Takım Adı",
"totalMembers": "Toplam Üye",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Ödeme hazırlanıyor...",
"redirecting": "Güvenli ödeme sayfasına yönlendiriliyor...",
"upgradeTitle": "{{planName}} planına yükselt",
"success": "Ödeme Başarılı!",
"successMessage": "Aboneliğiniz başarıyla etkinleştirildi. Kısa süre içinde bir onay e-postası alacaksınız.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Параметри"
},
"workspace": {
"title": "Робочий простір",
"people": "Учасники",
"teams": "Команди"
},
"configuration": {
"title": "Конфігурація",
"systemSettings": "Системні налаштування",
"features": "Функції",
"endpoints": "Кінцеві точки",
"database": "База даних",
"advanced": "Додатково"
},
"securityAuth": {
"title": "Безпека та автентифікація",
"security": "Безпека",
"connections": "Підключення"
},
"licensingAnalytics": {
"title": "Ліцензування та аналітика",
"plan": "План",
"audit": "Аудит",
"usageAnalytics": "Аналітика використання"
},
"policiesPrivacy": {
"title": "Політики та конфіденційність",
"legal": "Правові",
"privacy": "Конфіденційність"
},
"developer": {
"title": "Розробка",
"apiKeys": "Ключі API"
},
"tooltips": {
"enableLoginFirst": "Спочатку увімкніть режим входу",
"requiresEnterprise": "Потрібна ліцензія Enterprise"
},
"connection": {
"title": "Режим підключення",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Прибирати вимкнені опції перетворення в інструменті Convert замість показу сірим."
},
"hotkeys": {
"title": "Комбінації клавіш",
"description": "Налаштуйте комбінації клавіш для швидкого доступу до інструментів. Клацніть \"Change shortcut\" і натисніть нову комбінацію клавіш. Натисніть Esc, щоб скасувати.",
"errorConflict": "Комбінацію вже використовує {{tool}}.",
"errorModifier": {
"mac": "Додайте ⌘ (Command), ⌥ (Option) або інший модифікатор до комбінації.",
"windows": "Додайте Ctrl, Alt або інший модифікатор до комбінації."
},
"searchPlaceholder": "Пошук інструментів...",
"none": "Не призначено",
"customBadge": "Користувацька",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Доступні мови",
"description": "Мови, з яких користувачі можуть обирати (залиште порожнім, щоб увімкнути всі мови)"
"description": "Мови, з яких користувачі можуть обирати (залиште порожнім, щоб увімкнути всі мови)",
"placeholder": "Виберіть мови"
},
"customMetadata": {
"label": "Користувацькі метадані",
@@ -4061,7 +4108,9 @@
"label": "Стиль логотипа",
"description": "Оберіть між сучасним мінімалістичним логотипом або класичною іконкою S",
"classic": "Класичний",
"modern": "Сучасний"
"classicAlt": "Класичний логотип",
"modern": "Сучасний",
"modernAlt": "Сучасний логотип"
},
"customPaths": {
"label": "Користувацькі шляхи",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Конфігурація застосунку",
"description": "Поточні налаштування застосунку та подробиці конфігурації."
"description": "Поточні налаштування застосунку та подробиці конфігурації.",
"loading": "Завантаження конфігурації...",
"error": "Помилка",
"warning": "Попередження щодо конфігурації",
"sections": {
"basic": "Базова конфігурація",
"security": "Конфігурація безпеки",
"system": "Системна конфігурація",
"integration": "Конфігурація інтеграції"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Керуйте командами та організовуйте учасників робочого простору",
"loading": "Завантаження команд...",
"loadingDetails": "Завантаження відомостей про команду...",
"loadError": "Не вдалося завантажити дані команди",
"createNewTeam": "Створити нову команду",
"teamName": "Назва команди",
"totalMembers": "Усього учасників",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Підготовка оплати...",
"redirecting": "Переадресація на безпечну оплату...",
"upgradeTitle": "Оновити до {{planName}}",
"success": "Платіж успішний!",
"successMessage": "Вашу підписку успішно активовано. Незабаром ви отримаєте лист-підтвердження.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "Tùy chọn"
},
"workspace": {
"title": "Không gian làm việc",
"people": "Thành viên",
"teams": "Nhóm"
},
"configuration": {
"title": "Cấu hình",
"systemSettings": "Cài đặt hệ thống",
"features": "Tính năng",
"endpoints": "Endpoints",
"database": "Cơ sở dữ liệu",
"advanced": "Nâng cao"
},
"securityAuth": {
"title": "Bảo mật & Xác thực",
"security": "Bảo mật",
"connections": "Kết nối"
},
"licensingAnalytics": {
"title": "Giấy phép & Phân tích",
"plan": "Gói",
"audit": "Audit",
"usageAnalytics": "Phân tích sử dụng"
},
"policiesPrivacy": {
"title": "Chính sách & Quyền riêng tư",
"legal": "Pháp lý",
"privacy": "Quyền riêng tư"
},
"developer": {
"title": "Nhà phát triển",
"apiKeys": "API Keys"
},
"tooltips": {
"enableLoginFirst": "Bật chế độ đăng nhập trước",
"requiresEnterprise": "Yêu cầu giấy phép Enterprise"
},
"connection": {
"title": "Chế độ kết nối",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "Loại bỏ tùy chọn chuyển đổi bị vô hiệu hóa trong công cụ Chuyển đổi thay vì hiển thị mờ."
},
"hotkeys": {
"title": "Phím tắt",
"description": "Tùy chỉnh phím tắt để truy cập công cụ nhanh. Nhấp \"Change shortcut\" và nhấn một tổ hợp phím mới. Nhấn Esc để hủy.",
"errorConflict": "Phím tắt đã được dùng bởi {{tool}}.",
"errorModifier": {
"mac": "Bao gồm ⌘ (Command), ⌥ (Option), hoặc một phím bổ trợ khác trong phím tắt của bạn.",
"windows": "Bao gồm Ctrl, Alt, hoặc phím bổ trợ khác trong phím tắt của bạn."
},
"searchPlaceholder": "Tìm công cụ...",
"none": "Chưa gán",
"customBadge": "Tùy chỉnh",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "Ngôn ngữ khả dụng",
"description": "Ngôn ngữ người dùng có thể chọn (để trống để bật tất cả ngôn ngữ)"
"description": "Ngôn ngữ người dùng có thể chọn (để trống để bật tất cả ngôn ngữ)",
"placeholder": "Chọn ngôn ngữ"
},
"customMetadata": {
"label": "Siêu dữ liệu tùy chỉnh",
@@ -4061,7 +4108,9 @@
"label": "Kiểu logo",
"description": "Chọn giữa logo tối giản hiện đại hoặc biểu tượng S cổ điển",
"classic": "Cổ điển",
"modern": "Hiện đại"
"classicAlt": "Logo cổ điển",
"modern": "Hiện đại",
"modernAlt": "Logo hiện đại"
},
"customPaths": {
"label": "Đường dẫn tùy chỉnh",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "Cấu hình ứng dụng",
"description": "Cài đặt hiện tại và chi tiết cấu hình ứng dụng."
"description": "Cài đặt hiện tại và chi tiết cấu hình ứng dụng.",
"loading": "Đang tải cấu hình...",
"error": "Lỗi",
"warning": "Cảnh báo cấu hình",
"sections": {
"basic": "Cấu hình cơ bản",
"security": "Cấu hình bảo mật",
"system": "Cấu hình hệ thống",
"integration": "Cấu hình tích hợp"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "Quản lý nhóm và tổ chức thành viên trong không gian làm việc",
"loading": "Đang tải nhóm...",
"loadingDetails": "Đang tải chi tiết nhóm...",
"loadError": "Không thể tải chi tiết nhóm",
"createNewTeam": "Tạo nhóm mới",
"teamName": "Tên nhóm",
"totalMembers": "Tổng số thành viên",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "Đang chuẩn bị thanh toán của bạn...",
"redirecting": "Đang chuyển đến trang thanh toán an toàn...",
"upgradeTitle": "Nâng cấp lên {{planName}}",
"success": "Thanh toán thành công!",
"successMessage": "Gói đăng ký của bạn đã được kích hoạt thành công. Bạn sẽ sớm nhận được email xác nhận.",
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "首选项"
},
"workspace": {
"title": "工作区",
"people": "成员",
"teams": "团队"
},
"configuration": {
"title": "配置",
"systemSettings": "系统设置",
"features": "功能",
"endpoints": "端点",
"database": "数据库",
"advanced": "高级"
},
"securityAuth": {
"title": "安全与认证",
"security": "安全",
"connections": "连接"
},
"licensingAnalytics": {
"title": "许可与分析",
"plan": "方案",
"audit": "审计",
"usageAnalytics": "使用分析"
},
"policiesPrivacy": {
"title": "政策与隐私",
"legal": "法律",
"privacy": "隐私"
},
"developer": {
"title": "开发者",
"apiKeys": "API 密钥"
},
"tooltips": {
"enableLoginFirst": "请先启用登录模式",
"requiresEnterprise": "需要企业版许可"
},
"connection": {
"title": "连接模式",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "在转换工具中移除已禁用的选项,而不是以灰色显示。"
},
"hotkeys": {
"title": "键盘快捷键",
"description": "自定义键盘快捷键以快速访问工具。点击“更改快捷键”,然后按下新的组合键。按 Esc 取消。",
"errorConflict": "快捷键已被 {{tool}} 使用。",
"errorModifier": {
"mac": "快捷键需包含 ⌘(Command)、⌥(Option)或其他修饰键。",
"windows": "快捷键需包含 Ctrl、Alt 或其他修饰键。"
},
"searchPlaceholder": "搜索工具...",
"none": "未分配",
"customBadge": "自定义",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "可用语言",
"description": "用户可选择的语言(留空以启用所有语言)"
"description": "用户可选择的语言(留空以启用所有语言)",
"placeholder": "选择语言"
},
"customMetadata": {
"label": "自定义元数据",
@@ -4061,7 +4108,9 @@
"label": "Logo 样式",
"description": "选择现代极简 Logo 或经典 S 图标",
"classic": "经典",
"modern": "现代"
"classicAlt": "经典 Logo",
"modern": "现代",
"modernAlt": "现代 Logo"
},
"customPaths": {
"label": "自定义路径",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "应用配置",
"description": "当前应用设置和配置信息。"
"description": "当前应用设置和配置信息。",
"loading": "正在加载配置...",
"error": "错误",
"warning": "配置警告",
"sections": {
"basic": "基础配置",
"security": "安全配置",
"system": "系统配置",
"integration": "集成配置"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "管理团队并组织工作区成员",
"loading": "正在加载团队...",
"loadingDetails": "正在加载团队详情...",
"loadError": "无法加载团队详情",
"createNewTeam": "创建新团队",
"teamName": "团队名称",
"totalMembers": "成员总数",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "正在准备结账...",
"redirecting": "正在跳转到安全结账...",
"upgradeTitle": "升级到 {{planName}}",
"success": "支付成功!",
"successMessage": "您的订阅已成功激活。您很快会收到确认邮件。",
+64 -4
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "首选项"
},
"workspace": {
"title": "工作区",
"people": "成员",
"teams": "团队"
},
"configuration": {
"title": "配置",
"systemSettings": "系统设置",
"features": "功能",
"endpoints": "端点",
"database": "数据库",
"advanced": "高级"
},
"securityAuth": {
"title": "安全与认证",
"security": "安全",
"connections": "连接"
},
"licensingAnalytics": {
"title": "许可与分析",
"plan": "套餐",
"audit": "审计",
"usageAnalytics": "使用分析"
},
"policiesPrivacy": {
"title": "策略与隐私",
"legal": "法律",
"privacy": "隐私"
},
"developer": {
"title": "开发者",
"apiKeys": "API 密钥"
},
"tooltips": {
"enableLoginFirst": "请先启用登录模式",
"requiresEnterprise": "需要企业版许可证"
},
"connection": {
"title": "连接模式",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "在“转换”工具中移除已禁用的转换选项,而非灰显显示。"
},
"hotkeys": {
"title": "键盘快捷键",
"description": "自定义键盘快捷键以快速访问工具。点击 \"更改快捷键\",然后按下新的按键组合。按 Esc 取消。",
"errorConflict": "快捷键已被 {{tool}} 使用。",
"errorModifier": {
"mac": "请在快捷键中包含 ⌘(Command)、⌥(Option)或其他修饰键。",
"windows": "请在快捷键中包含 Ctrl、Alt 或其他修饰键。"
},
"searchPlaceholder": "搜索工具...",
"none": "未分配",
"customBadge": "自定义",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "可用语言",
"description": "用户可选择的语言(留空启用所有语言)"
"description": "用户可选择的语言(留空启用所有语言)",
"placeholder": "选择语言"
},
"customMetadata": {
"label": "自定义元数据",
@@ -4061,7 +4108,9 @@
"label": "徽标样式",
"description": "在现代极简徽标与经典S图标之间选择",
"classic": "经典",
"modern": "现代"
"classicAlt": "经典 Logo",
"modern": "现代",
"modernAlt": "现代 Logo"
},
"customPaths": {
"label": "自定义路径",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "应用配置",
"description": "当前应用设置与配置信息。"
"description": "当前应用设置与配置信息。",
"loading": "正在加载配置...",
"error": "错误",
"warning": "配置警告",
"sections": {
"basic": "基础配置",
"security": "安全配置",
"system": "系统配置",
"integration": "集成配置"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "管理团队并组织工作区成员",
"loading": "正在加载团队...",
"loadingDetails": "正在加载团队详情...",
"loadError": "加载团队详情失败",
"createNewTeam": "创建新团队",
"teamName": "团队名称",
"totalMembers": "成员总数",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "正在准备结账...",
"redirecting": "正在跳转到安全结账...",
"upgradeTitle": "升级到 {{planName}}",
"success": "支付成功!",
"successMessage": "您的订阅已成功激活。您将很快收到确认邮件。",
@@ -5840,7 +5900,7 @@
"placeholder": "输入用户名"
},
"email": {
"label": "Email",
"label": "邮箱",
"placeholder": "输入 Email"
},
"password": {
+63 -3
View File
@@ -317,6 +317,46 @@
}
},
"settings": {
"preferences": {
"title": "偏好設定"
},
"workspace": {
"title": "工作空間",
"people": "成員",
"teams": "團隊"
},
"configuration": {
"title": "組態",
"systemSettings": "系統設定",
"features": "功能",
"endpoints": "端點",
"database": "資料庫",
"advanced": "進階"
},
"securityAuth": {
"title": "安全性與身分驗證",
"security": "安全性",
"connections": "連線"
},
"licensingAnalytics": {
"title": "授權與分析",
"plan": "方案",
"audit": "稽核",
"usageAnalytics": "使用分析"
},
"policiesPrivacy": {
"title": "政策與隱私",
"legal": "法務",
"privacy": "隱私"
},
"developer": {
"title": "開發者",
"apiKeys": "API 金鑰"
},
"tooltips": {
"enableLoginFirst": "先啟用登入模式",
"requiresEnterprise": "需要 Enterprise 授權"
},
"connection": {
"title": "連線模式",
"mode": {
@@ -373,7 +413,13 @@
"hideUnavailableConversionsDescription": "在「轉換」工具中移除已停用的轉換選項,而非以灰色顯示。"
},
"hotkeys": {
"title": "鍵盤快速鍵",
"description": "自訂鍵盤快速鍵以快速存取工具。按「變更快速鍵」,然後按下新的按鍵組合。按 Esc 取消。",
"errorConflict": "快捷鍵已被 {{tool}} 使用。",
"errorModifier": {
"mac": "請在快速鍵中包含 ⌘(Command)、⌥(Option)或其他修飾鍵。",
"windows": "請在快速鍵中包含 Ctrl、Alt 或其他修飾鍵。"
},
"searchPlaceholder": "搜尋工具...",
"none": "未指派",
"customBadge": "自訂",
@@ -4036,7 +4082,8 @@
},
"languages": {
"label": "可用語言",
"description": "使用者可選擇的語言(留空以啟用所有語言)"
"description": "使用者可選擇的語言(留空以啟用所有語言)",
"placeholder": "選擇語言"
},
"customMetadata": {
"label": "自訂中繼資料",
@@ -4061,7 +4108,9 @@
"label": "標誌樣式",
"description": "在現代極簡標誌與經典 S 圖示之間選擇",
"classic": "經典",
"modern": "現代"
"classicAlt": "經典 Logo",
"modern": "現代",
"modernAlt": "現代 Logo"
},
"customPaths": {
"label": "自訂路徑",
@@ -4968,7 +5017,16 @@
"config": {
"overview": {
"title": "應用程式設定",
"description": "目前的應用程式設定與詳細資訊。"
"description": "目前的應用程式設定與詳細資訊。",
"loading": "正在載入組態...",
"error": "錯誤",
"warning": "組態警告",
"sections": {
"basic": "基本組態",
"security": "安全性組態",
"system": "系統組態",
"integration": "整合組態"
}
},
"account": {
"overview": {
@@ -5342,6 +5400,7 @@
"description": "管理團隊並組織工作空間成員",
"loading": "正在載入團隊...",
"loadingDetails": "正在載入團隊詳細資料...",
"loadError": "無法載入團隊詳細資料",
"createNewTeam": "建立新團隊",
"teamName": "團隊名稱",
"totalMembers": "成員總數",
@@ -5544,6 +5603,7 @@
},
"payment": {
"preparing": "正在準備結帳...",
"redirecting": "正在重新導向至安全結帳...",
"upgradeTitle": "升級至 {{planName}}",
"success": "付款成功!",
"successMessage": "您的訂閱已成功啟用。您將很快收到確認電子郵件。",
+79 -14
View File
@@ -4250,6 +4250,7 @@ dependencies = [
"tokio",
"url",
"urlencoding",
"windows 0.58.0",
]
[[package]]
@@ -4437,7 +4438,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
@@ -4514,7 +4515,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -4683,7 +4684,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.17",
"url",
"windows",
"windows 0.61.3",
"zbus",
]
@@ -4761,7 +4762,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
]
[[package]]
@@ -4787,7 +4788,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.3",
"wry",
]
@@ -5624,10 +5625,10 @@ checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
]
[[package]]
@@ -5648,7 +5649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c"
dependencies = [
"thiserror 2.0.17",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
]
@@ -5698,6 +5699,16 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.3"
@@ -5720,14 +5731,27 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
@@ -5739,8 +5763,8 @@ version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-implement 0.60.2",
"windows-interface 0.59.3",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
@@ -5757,6 +5781,17 @@ dependencies = [
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
@@ -5768,6 +5803,17 @@ dependencies = [
"syn 2.0.108",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.108",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
@@ -5812,6 +5858,15 @@ dependencies = [
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -5830,6 +5885,16 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@@ -6252,7 +6317,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.61.3",
"windows-core 0.61.2",
"windows-version",
"x11-dl",
+8
View File
@@ -45,3 +45,11 @@ rand = "0.8"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.10"
core-services = "1.0"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_UI_Shell",
"Win32_System_ApplicationInstallationAndServicing",
] }
+67 -36
View File
@@ -1,8 +1,5 @@
use crate::utils::add_log;
#[cfg(any(target_os = "windows", target_os = "linux"))]
use std::process::Command;
/// Check if Stirling PDF is the default PDF handler
#[tauri::command]
pub fn is_default_pdf_handler() -> Result<bool, String> {
@@ -51,50 +48,80 @@ pub fn set_as_default_pdf_handler() -> Result<String, String> {
#[cfg(target_os = "windows")]
fn check_default_windows() -> Result<bool, String> {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
use windows::core::HSTRING;
use windows::Win32::Foundation::RPC_E_CHANGED_MODE;
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_INPROC_SERVER,
COINIT_APARTMENTTHREADED,
};
use windows::Win32::UI::Shell::{
IApplicationAssociationRegistration, ApplicationAssociationRegistration,
ASSOCIATIONTYPE, ASSOCIATIONLEVEL,
};
// Query the default handler for .pdf extension
let output = Command::new("cmd")
.args(["/C", "assoc .pdf"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to check default app: {}", e))?;
unsafe {
// Initialize COM for this thread
let hr = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
// RPC_E_CHANGED_MODE means COM is already initialized, which is fine
if hr.is_err() && hr != RPC_E_CHANGED_MODE {
return Err(format!("Failed to initialize COM: {:?}", hr));
}
let assoc = String::from_utf8_lossy(&output.stdout);
add_log(format!("Windows PDF association: {}", assoc.trim()));
let result = (|| -> Result<bool, String> {
// Create the IApplicationAssociationRegistration instance
let reg: IApplicationAssociationRegistration =
CoCreateInstance(&ApplicationAssociationRegistration, None, CLSCTX_INPROC_SERVER)
.map_err(|e| format!("Failed to create COM instance: {}", e))?;
// Get the ProgID for .pdf files
if let Some(prog_id) = assoc.trim().strip_prefix(".pdf=") {
// Query what application handles this ProgID
let output = Command::new("cmd")
.args(["/C", &format!("ftype {}", prog_id)])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map_err(|e| format!("Failed to query file type: {}", e))?;
// Query the current default handler for .pdf extension
let extension = HSTRING::from(".pdf");
let ftype = String::from_utf8_lossy(&output.stdout);
add_log(format!("Windows file type: {}", ftype.trim()));
let default_app = reg.QueryCurrentDefault(
&extension,
ASSOCIATIONTYPE(0), // AT_FILEEXTENSION
ASSOCIATIONLEVEL(1), // AL_EFFECTIVE - gets the effective default (user or machine level)
)
.map_err(|e| format!("Failed to query current default: {}", e))?;
// Check if it contains "Stirling" or our app name
let is_default = ftype.to_lowercase().contains("stirling");
Ok(is_default)
} else {
Ok(false)
// Convert PWSTR to String
let default_str = default_app.to_string()
.map_err(|e| format!("Failed to convert default app string: {}", e))?;
add_log(format!("Windows PDF handler ProgID: {}", default_str));
// Check if it contains "Stirling" (case-insensitive)
// Note: This checks the ProgID registered by the installer
let is_default = default_str.to_lowercase().contains("stirling");
Ok(is_default)
})();
// Clean up COM
CoUninitialize();
result
}
}
#[cfg(target_os = "windows")]
fn set_default_windows() -> Result<String, String> {
// On Windows 10+, we need to open the Default Apps settings
// as programmatic setting requires a signed installer
Command::new("cmd")
.args(["/C", "start", "ms-settings:defaultapps"])
.spawn()
.map_err(|e| format!("Failed to open default apps settings: {}", e))?;
use std::process::Command;
add_log("Opened Windows Default Apps settings".to_string());
Ok("opened_settings".to_string())
// Windows 10+ approach: Open Settings app directly to default apps
// This is more reliable than COM APIs which require pre-registration
// ms-settings:defaultapps opens the default apps settings page
let result = Command::new("cmd")
.args(["/C", "start", "ms-settings:defaultapps"])
.output()
.map_err(|e| format!("Failed to open Windows Settings: {}", e))?;
if result.status.success() {
add_log("Opened Windows default apps settings".to_string());
Ok("opened_dialog".to_string())
} else {
let error = String::from_utf8_lossy(&result.stderr);
add_log(format!("Failed to open settings: {}", error));
Err(format!("Failed to open default apps settings: {}", error))
}
}
// ============================================================================
@@ -184,6 +211,8 @@ fn set_default_macos() -> Result<String, String> {
#[cfg(target_os = "linux")]
fn check_default_linux() -> Result<bool, String> {
use std::process::Command;
// Use xdg-mime to check the default application for PDF files
let output = Command::new("xdg-mime")
.args(["query", "default", "application/pdf"])
@@ -200,6 +229,8 @@ fn check_default_linux() -> Result<bool, String> {
#[cfg(target_os = "linux")]
fn set_default_linux() -> Result<String, String> {
use std::process::Command;
// Use xdg-mime to set the default application for PDF files
let result = Command::new("xdg-mime")
.args(["default", "stirling-pdf.desktop", "application/pdf"])
@@ -12,6 +12,7 @@ import { FileManagerProvider } from '@app/contexts/FileManagerContext';
import { Z_INDEX_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
import { isGoogleDriveConfigured } from '@app/services/googleDrivePickerService';
import { loadScript } from '@app/utils/scriptLoader';
import { useAllFiles } from '@app/contexts/FileContext';
interface FileManagerProps {
selectedTool?: Tool | null;
@@ -25,6 +26,9 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
const { loadRecentFiles, handleRemoveFile, loading } = useFileManager();
// Get active file IDs from FileContext to show which files are already loaded
const { fileIds: activeFileIds } = useAllFiles();
// File management handlers
const isFileSupported = useCallback((fileName: string) => {
if (!selectedTool?.supportedFormats) return true;
@@ -175,6 +179,7 @@ const FileManager: React.FC<FileManagerProps> = ({ selectedTool }) => {
modalHeight={modalHeight}
refreshRecentFiles={refreshRecentFiles}
isLoading={loading}
activeFileIds={activeFileIds}
>
{isMobile ? <MobileLayout /> : <DesktopLayout />}
</FileManagerProvider>
@@ -30,6 +30,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
onDownloadSingle,
isFileSupported,
isLoading,
activeFileIds,
} = useFileManagerContext();
const { t } = useTranslation();
@@ -55,6 +56,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
// All files in filteredFiles are now leaf files only
const historyFiles = loadedHistoryFiles.get(file.id) || [];
const isExpanded = expandedFileIds.has(file.id);
const isActive = activeFileIds.includes(file.id);
return (
<React.Fragment key={file.id}>
@@ -68,6 +70,7 @@ const FileListArea: React.FC<FileListAreaProps> = ({
onDoubleClick={() => onFileDoubleClick(file)}
isHistoryFile={false} // All files here are leaf files
isLatestVersion={true} // All files here are the latest versions
isActive={isActive}
/>
<FileHistoryGroup
@@ -6,6 +6,7 @@ import DownloadIcon from '@mui/icons-material/Download';
import HistoryIcon from '@mui/icons-material/History';
import RestoreIcon from '@mui/icons-material/Restore';
import UnarchiveIcon from '@mui/icons-material/Unarchive';
import CloseIcon from '@mui/icons-material/Close';
import { useTranslation } from 'react-i18next';
import { getFileSize, getFileDate } from '@app/utils/fileUtils';
import { FileId, StirlingFileStub } from '@app/types/fileContext';
@@ -14,6 +15,7 @@ import { zipFileService } from '@app/services/zipFileService';
import ToolChain from '@app/components/shared/ToolChain';
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
import { PrivateContent } from '@app/components/shared/PrivateContent';
import { useFileManagement } from '@app/contexts/FileContext';
interface FileListItemProps {
file: StirlingFileStub;
@@ -26,6 +28,7 @@ interface FileListItemProps {
isLast?: boolean;
isHistoryFile?: boolean; // Whether this is a history file (indented)
isLatestVersion?: boolean; // Whether this is the latest version (shows chevron)
isActive?: boolean; // Whether this file is currently loaded in FileContext
}
const FileListItem: React.FC<FileListItemProps> = ({
@@ -37,12 +40,14 @@ const FileListItem: React.FC<FileListItemProps> = ({
onDownload,
onDoubleClick,
isHistoryFile = false,
isLatestVersion = false
isLatestVersion = false,
isActive = false
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const {expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext();
const { removeFiles } = useFileManagement();
// Check if this is a ZIP file
const isZipFile = zipFileService.isZipFileStub(file);
@@ -61,8 +66,10 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Box
p="sm"
style={{
cursor: isHistoryFile ? 'default' : 'pointer',
backgroundColor: isSelected
cursor: isHistoryFile || isActive ? 'default' : 'pointer',
backgroundColor: isActive
? 'var(--file-active-bg)'
: isSelected
? 'var(--mantine-color-gray-1)'
: (shouldShowHovered ? 'var(--mantine-color-gray-1)' : 'var(--bg-file-list)'),
opacity: isSupported ? 1 : 0.5,
@@ -74,7 +81,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
paddingLeft: isHistoryFile ? '2rem' : '0.75rem', // Indent history files
borderLeft: isHistoryFile ? '3px solid var(--mantine-color-blue-4)' : 'none' // Visual indicator for history
}}
onClick={isHistoryFile ? undefined : (e) => onSelect(e.shiftKey)}
onClick={isHistoryFile || isActive ? undefined : (e) => onSelect(e.shiftKey)}
onDoubleClick={onDoubleClick}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@@ -84,14 +91,16 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Box>
{/* Checkbox for regular files only */}
<Checkbox
checked={isSelected}
checked={isActive || isSelected}
onChange={() => {}} // Handled by parent onClick
size="sm"
pl="sm"
pr="xs"
disabled={isActive}
color={isActive ? "green" : undefined}
styles={{
input: {
cursor: 'pointer'
cursor: isActive ? 'not-allowed' : 'pointer'
}
}}
/>
@@ -103,6 +112,19 @@ const FileListItem: React.FC<FileListItemProps> = ({
<Text size="sm" fw={500} truncate style={{ flex: 1 }}>
<PrivateContent>{file.name}</PrivateContent>
</Text>
{isActive && (
<Badge
size="xs"
variant="light"
style={{
backgroundColor: 'var(--file-active-badge-bg)',
color: 'var(--file-active-badge-fg)',
border: '1px solid var(--file-active-badge-border)'
}}
>
{t('fileManager.active', 'Active')}
</Badge>
)}
<Badge size="xs" variant="light" color={"blue"}>
v{currentVersion}
</Badge>
@@ -151,6 +173,22 @@ const FileListItem: React.FC<FileListItemProps> = ({
</Menu.Target>
<Menu.Dropdown>
{/* Close file option for active files */}
{isActive && (
<>
<Menu.Item
leftSection={<CloseIcon style={{ fontSize: 16 }} />}
onClick={(e) => {
e.stopPropagation();
removeFiles([file.id]);
}}
>
{t('fileManager.closeFile', 'Close File')}
</Menu.Item>
<Menu.Divider />
</>
)}
{onDownload && (
<Menu.Item
leftSection={<DownloadIcon style={{ fontSize: 16 }} />}
@@ -34,7 +34,7 @@ export default function InitialOnboardingModal(props: InitialOnboardingModalProp
return (
<div className={styles.heroIconsContainer}>
<div className={styles.iconWrapper}>
<img src={`${BASE_PATH}/branding/StirlingLogoLegacy.svg`} alt="Stirling icon" className={styles.downloadIcon} />
<img src={`${BASE_PATH}/modern-logo/logo512.png`} alt="Stirling icon" className={styles.downloadIcon} />
</div>
</div>
);
@@ -71,8 +71,10 @@ export default function ServerLicenseModal({
slideKey={slide.key}
/>
<div className={styles.heroLogo}>
<div className={styles.heroLogoCircle}>
<img src={`${BASE_PATH}/branding/StirlingPDFLogoNoTextLightHC.svg`} alt="Stirling logo" />
<div className={styles.heroIconsContainer}>
<div className={styles.iconWrapper}>
<img src={`${BASE_PATH}/modern-logo/logo512.png`} alt="Stirling icon" className={styles.downloadIcon} />
</div>
</div>
</div>
</div>
@@ -4,7 +4,7 @@ import { useAppConfig } from '@app/contexts/AppConfigContext';
import { useCookieConsentContext } from '@app/contexts/CookieConsentContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import type { LicenseNotice } from '@app/types/types';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import {
ONBOARDING_SESSION_BLOCK_KEY,
ONBOARDING_SESSION_EVENT,
@@ -13,6 +13,15 @@ import {
} from '@app/constants/events';
import { useServerExperience } from '@app/hooks/useServerExperience';
// Auth routes where onboarding should NOT show
const AUTH_ROUTES = ['/login', '/signup', '/auth', '/invite'];
// Check if user has an auth token (to avoid flash before redirect)
function hasAuthToken(): boolean {
if (typeof window === 'undefined') return false;
return !!localStorage.getItem('stirling_jwt');
}
interface InitialModalHandlers {
opened: boolean;
onLicenseNoticeUpdate: (notice: LicenseNotice) => void;
@@ -29,11 +38,30 @@ interface ServerLicenseModalHandlers {
export function useOnboardingFlow() {
const { preferences, updatePreference } = usePreferences();
const { config } = useAppConfig();
const { config, loading: configLoading } = useAppConfig();
const { showCookieConsent, isReady: isCookieConsentReady } = useCookieConsentContext();
const { completeTour, tourType, isOpen } = useOnboarding();
const location = useLocation();
const shouldShowIntro = !preferences.hasSeenIntroOnboarding;
// Check if we're on an auth route (login, signup, etc.)
const isOnAuthRoute = AUTH_ROUTES.some(route => location.pathname.startsWith(route));
// Check if login is enabled but user doesn't have a token
// This prevents a flash of the modal before redirect to /login
const loginEnabled = config?.enableLogin === true;
const isUnauthenticatedWithLoginEnabled = loginEnabled && !hasAuthToken();
// Don't show intro onboarding:
// 1. On explicit auth routes (/login, /signup, etc.)
// 2. While config is still loading
// 3. When login is enabled but user isn't authenticated (would redirect to /login)
// This ensures:
// - If login is enabled: user must be logged in before seeing onboarding
// - If login is disabled: homepage must have rendered first
const shouldShowIntro = !preferences.hasSeenIntroOnboarding
&& !isOnAuthRoute
&& !configLoading
&& !isUnauthenticatedWithLoginEnabled;
const isAdminUser = !!config?.isAdmin;
const { hasPaidLicense } = useServerExperience();
@@ -175,7 +175,7 @@ export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
'server-license': {
id: 'server-license',
createSlide: ({ licenseNotice }) => ServerLicenseSlide({ licenseNotice }),
hero: { type: 'logo' },
hero: { type: 'dual-icon' },
buttons: [
{
key: 'license-close',
@@ -2,7 +2,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { Modal, Text, ActionIcon, Tooltip, Group } from '@mantine/core';
import { useNavigate, useLocation } from 'react-router-dom';
import LocalIcon from '@app/components/shared/LocalIcon';
import { createConfigNavSections } from '@app/components/shared/config/configNavSections';
import { useConfigNavSections } from '@app/components/shared/config/configNavSections';
import { NavKey, VALID_NAV_KEYS } from '@app/components/shared/config/types';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import '@app/components/shared/AppConfigModal.css';
@@ -74,13 +74,10 @@ const AppConfigModalInner: React.FC<AppConfigModalProps> = ({ opened, onClose })
const loginEnabled = config?.enableLogin ?? false;
// Left navigation structure and icons
const configNavSections = useMemo(() =>
createConfigNavSections(
isAdmin,
runningEE,
loginEnabled
),
[isAdmin, runningEE, loginEnabled]
const configNavSections = useConfigNavSections(
isAdmin,
runningEE,
loginEnabled
);
const activeLabel = useMemo(() => {
@@ -130,6 +130,11 @@ export const InfoBanner: React.FC<InfoBannerProps> = ({
onClick={onButtonClick}
loading={loading}
leftSection={<LocalIcon icon={buttonIcon} width="0.9rem" height="0.9rem" />}
styles={{
label: {
color: textColor ?? toneStyle.text,
},
}}
>
{buttonText}
</Button>
@@ -1,4 +1,5 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { NavKey } from '@app/components/shared/config/types';
import HotkeysSection from '@app/components/shared/config/configSections/HotkeysSection';
import GeneralSection from '@app/components/shared/config/configSections/GeneralSection';
@@ -27,11 +28,43 @@ export interface ConfigColors {
headerBorder: string;
}
export const useConfigNavSections = (
_isAdmin: boolean = false,
_runningEE: boolean = false,
_loginEnabled: boolean = false
): ConfigNavSection[] => {
const { t } = useTranslation();
const sections: ConfigNavSection[] = [
{
title: t('settings.preferences.title', 'Preferences'),
items: [
{
key: 'general',
label: t('settings.general.title', 'General'),
icon: 'settings-rounded',
component: <GeneralSection />
},
{
key: 'hotkeys',
label: t('settings.hotkeys.title', 'Keyboard Shortcuts'),
icon: 'keyboard-rounded',
component: <HotkeysSection />
},
],
},
];
return sections;
};
// Deprecated: Use useConfigNavSections hook instead
export const createConfigNavSections = (
_isAdmin: boolean = false,
_runningEE: boolean = false,
_loginEnabled: boolean = false
): ConfigNavSection[] => {
console.warn('createConfigNavSections is deprecated. Use useConfigNavSections hook instead for proper i18n support.');
const sections: ConfigNavSection[] = [
{
title: 'Preferences',
@@ -73,10 +73,7 @@ const HotkeysSection: React.FC = () => {
const binding = eventToBinding(event as KeyboardEvent);
if (!binding) {
const osKey = isMac ? 'mac' : 'windows';
const fallbackText = isMac
? 'Include ⌘ (Command), ⌥ (Option), or another modifier in your shortcut.'
: 'Include Ctrl, Alt, or another modifier in your shortcut.';
setError(t(`settings.hotkeys.errorModifier.${osKey}`, fallbackText));
setError(t(`settings.hotkeys.errorModifier.${osKey}`));
return;
}
@@ -112,9 +109,9 @@ const HotkeysSection: React.FC = () => {
return (
<Stack gap="lg">
<div>
<Text fw={600} size="lg">Keyboard Shortcuts</Text>
<Text fw={600} size="lg">{t('settings.hotkeys.title', 'Keyboard Shortcuts')}</Text>
<Text size="sm" c="dimmed">
Customize keyboard shortcuts for quick tool access. Click "Change shortcut" and press a new key combination. Press Esc to cancel.
{t('settings.hotkeys.description', 'Customize keyboard shortcuts for quick tool access. Click "Change shortcut" and press a new key combination. Press Esc to cancel.')}
</Text>
</div>
@@ -1,9 +1,11 @@
import React from 'react';
import { Stack, Text, Code, Group, Badge, Alert, Loader } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useAppConfig } from '@app/contexts/AppConfigContext';
import { OverviewHeader } from '@app/components/shared/config/OverviewHeader';
const Overview: React.FC = () => {
const { t } = useTranslation();
const { config, loading, error } = useAppConfig();
const renderConfigSection = (title: string, data: any) => {
@@ -58,14 +60,14 @@ const Overview: React.FC = () => {
return (
<Stack align="center" py="md">
<Loader size="sm" />
<Text size="sm" c="dimmed">Loading configuration...</Text>
<Text size="sm" c="dimmed">{t('config.overview.loading', 'Loading configuration...')}</Text>
</Stack>
);
}
if (error) {
return (
<Alert color="red" title="Error">
<Alert color="red" title={t('config.overview.error', 'Error')}>
{error}
</Alert>
);
@@ -77,13 +79,13 @@ const Overview: React.FC = () => {
{config && (
<>
{renderConfigSection('Basic Configuration', basicConfig)}
{renderConfigSection('Security Configuration', securityConfig)}
{renderConfigSection('System Configuration', systemConfig)}
{renderConfigSection('Integration Configuration', integrationConfig)}
{renderConfigSection(t('config.overview.sections.basic', 'Basic Configuration'), basicConfig)}
{renderConfigSection(t('config.overview.sections.security', 'Security Configuration'), securityConfig)}
{renderConfigSection(t('config.overview.sections.system', 'System Configuration'), systemConfig)}
{renderConfigSection(t('config.overview.sections.integration', 'Integration Configuration'), integrationConfig)}
{config.error && (
<Alert color="yellow" title="Configuration Warning">
<Alert color="yellow" title={t('config.overview.warning', 'Configuration Warning')}>
{config.error}
</Alert>
)}
@@ -41,6 +41,8 @@ import { HistoryAPIBridge } from '@app/components/viewer/HistoryAPIBridge';
import type { SignatureAPI, HistoryAPI } from '@app/components/viewer/viewerTypes';
import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge';
import { BookmarkAPIBridge } from '@app/components/viewer/BookmarkAPIBridge';
import { isPdfFile } from '@app/utils/fileUtils';
import { useTranslation } from 'react-i18next';
interface LocalEmbedPDFProps {
file?: File | Blob;
@@ -52,6 +54,7 @@ interface LocalEmbedPDFProps {
}
export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
const { t } = useTranslation();
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
@@ -171,6 +174,29 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur
);
}
// Check if the file is actually a PDF
if (file && !isPdfFile(file)) {
const fileName = 'name' in file ? file.name : t('viewer.unknownFile');
return (
<Center h="100%" w="100%">
<Stack align="center" gap="md">
<div style={{ fontSize: '48px' }}>📄</div>
<Text size="lg" fw={600} c="dimmed">
{t('viewer.cannotPreviewFile')}
</Text>
<Text c="dimmed" size="sm" style={{ textAlign: 'center', maxWidth: '400px' }}>
{t('viewer.onlyPdfSupported')}
</Text>
<PrivateContent>
<Text c="dimmed" size="xs" style={{ fontFamily: 'monospace' }}>
{fileName}
</Text>
</PrivateContent>
</Stack>
</Center>
);
}
if (isLoading || !engine || !pdfUrl) {
return <ToolLoadingFallback toolName="PDF Engine" />;
}
@@ -51,6 +51,7 @@ describe('AppConfigContext', () => {
expect(apiClient.get).toHaveBeenCalledWith('/api/v1/config/app-config', {
suppressErrorToast: true,
skipAuthRedirect: true,
});
});
@@ -282,6 +283,7 @@ describe('AppConfigContext', () => {
await waitFor(() => {
expect(apiClient.get).toHaveBeenCalledWith('/api/v1/config/app-config', {
suppressErrorToast: true,
skipAuthRedirect: true,
});
});
});
@@ -87,7 +87,8 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
const isBlockingMode = bootstrapMode === 'blocking';
const [config, setConfig] = useState<AppConfig | null>(initialConfig);
const [error, setError] = useState<string | null>(null);
const [fetchCount, setFetchCount] = useState(0);
// Track how many times we've attempted to fetch. useRef avoids re-renders that can trigger loops.
const fetchCountRef = React.useRef(0);
const [hasResolvedConfig, setHasResolvedConfig] = useState(Boolean(initialConfig) && !isBlockingMode);
const [loading, setLoading] = useState(!hasResolvedConfig);
@@ -96,11 +97,14 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
const fetchConfig = useCallback(async (force = false) => {
// Prevent duplicate fetches unless forced
if (!force && fetchCount > 0) {
if (!force && fetchCountRef.current > 0) {
console.debug('[AppConfig] Already fetched, skipping');
return;
}
// Mark that we've attempted a fetch to prevent repeated auto-fetch loops
fetchCountRef.current += 1;
const shouldBlockUI = !hasResolvedConfig || isBlockingMode;
if (shouldBlockUI) {
setLoading(true);
@@ -112,7 +116,6 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
const testConfig = getSimulatedAppConfig();
if (testConfig) {
setConfig(testConfig);
setFetchCount((prev) => prev + 1);
setHasResolvedConfig(true);
setLoading(false);
return;
@@ -128,12 +131,17 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
// apiClient automatically adds JWT header if available via interceptors
// Always suppress error toast - we handle 401 errors locally
const response = await apiClient.get<AppConfig>('/api/v1/config/app-config', { suppressErrorToast: true });
const response = await apiClient.get<AppConfig>(
'/api/v1/config/app-config',
{
suppressErrorToast: true,
skipAuthRedirect: true,
} as any,
);
const data = response.data;
console.debug('[AppConfig] Config fetched successfully:', data);
setConfig(data);
setFetchCount(prev => prev + 1);
setHasResolvedConfig(true);
setLoading(false);
return; // Success - exit function
@@ -170,7 +178,7 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
}
setLoading(false);
}, [fetchCount, hasResolvedConfig, isBlockingMode, maxRetries, initialDelay]);
}, [hasResolvedConfig, isBlockingMode, maxRetries, initialDelay]);
useEffect(() => {
// Skip config fetch on auth pages (/login, /signup, /auth/callback, /invite/*)
@@ -209,11 +217,13 @@ export const AppConfigProvider: React.FC<AppConfigProviderProps> = ({
return () => window.removeEventListener('jwt-available', handleJwtAvailable);
}, [fetchConfig]);
const refetch = useCallback(() => fetchConfig(true), [fetchConfig]);
const value: AppConfigContextValue = {
config,
loading,
error,
refetch: () => fetchConfig(true),
refetch,
};
return (
@@ -20,6 +20,7 @@ interface FileManagerContextValue {
fileGroups: Map<FileId, StirlingFileStub[]>;
loadedHistoryFiles: Map<FileId, StirlingFileStub[]>;
isLoading: boolean;
activeFileIds: FileId[];
// Handlers
onSourceChange: (source: 'recent' | 'local' | 'drive') => void;
@@ -63,6 +64,7 @@ interface FileManagerProviderProps {
modalHeight: string;
refreshRecentFiles: () => Promise<void>;
isLoading: boolean;
activeFileIds: FileId[];
}
export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
@@ -77,6 +79,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
modalHeight,
refreshRecentFiles,
isLoading,
activeFileIds,
}) => {
const [activeSource, setActiveSource] = useState<'recent' | 'local' | 'drive'>('recent');
const [selectedFileIds, setSelectedFileIds] = useState<FileId[]>([]);
@@ -618,6 +621,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
fileGroups,
loadedHistoryFiles,
isLoading,
activeFileIds,
// Handlers
onSourceChange: handleSourceChange,
@@ -654,6 +658,7 @@ export const FileManagerProvider: React.FC<FileManagerProviderProps> = ({
fileGroups,
loadedHistoryFiles,
isLoading,
activeFileIds,
handleSourceChange,
handleLocalFileClick,
handleFileSelect,
@@ -0,0 +1,86 @@
import { useCallback, useEffect, useState } from 'react';
import { BASE_PATH } from '@app/constants/app';
type BackendStatus = 'up' | 'starting' | 'down';
interface BackendProbeState {
status: BackendStatus;
loginDisabled: boolean;
loading: boolean;
}
/**
* Lightweight backend probe that avoids global axios interceptors.
* Used on auth screens to decide whether to show login, anonymous mode, or a backend-starting message.
*/
export function useBackendProbe() {
const [state, setState] = useState<BackendProbeState>({
status: 'starting',
loginDisabled: false,
loading: true,
});
const probe = useCallback(async () => {
const statusUrl = `${BASE_PATH || ''}/api/v1/info/status`;
const loginUrl = `${BASE_PATH || ''}/api/v1/proprietary/ui-data/login`;
const next: BackendProbeState = {
status: 'starting',
loginDisabled: false,
loading: false,
};
try {
const res = await fetch(statusUrl, { method: 'GET', cache: 'no-store' });
if (res.ok) {
const data = await res.json().catch(() => null);
if (data && data.status === 'UP') {
next.status = 'up';
setState(next);
return next;
}
next.status = 'starting';
} else if (res.status === 404 || res.status === 503) {
next.status = 'starting';
} else {
next.status = 'down';
}
} catch {
next.status = 'down';
}
// Fallback: proprietary login endpoint to detect disabled login and backend availability
try {
const res = await fetch(loginUrl, { method: 'GET', cache: 'no-store' });
if (res.ok) {
next.status = 'up';
const data = await res.json().catch(() => null);
if (data && data.enableLogin === false) {
next.loginDisabled = true;
}
} else if (res.status === 404) {
// Endpoint missing usually means login disabled
next.status = 'up';
next.loginDisabled = true;
} else if (res.status === 503) {
next.status = 'starting';
} else {
next.status = 'down';
}
} catch {
// keep previous inferred state (down/starting)
}
setState(next);
return next;
}, []);
useEffect(() => {
void probe();
}, [probe]);
return {
...state,
probe,
};
}
+12 -4
View File
@@ -88,6 +88,7 @@ const SPECIAL_SUPPRESS_MS = 1500; // brief window to suppress generic duplicate
* Returns true if the error should be suppressed (deduplicated), false otherwise
*/
export async function handleHttpError(error: any): Promise<boolean> {
const skipAuthRedirect = error?.config?.skipAuthRedirect === true;
// Check if this error should skip the global toast (component will handle it)
if (error?.config?.suppressErrorToast === true) {
return false; // Don't show global toast, but continue rejection
@@ -105,12 +106,19 @@ export async function handleHttpError(error: any): Promise<boolean> {
pathname.includes('/invite/');
// If not on auth page, redirect to login with expired session message
if (!isAuthPage) {
if (!isAuthPage && !skipAuthRedirect) {
console.debug('[httpErrorHandler] 401 detected, redirecting to login');
// Store the current location so we can redirect back after login
const currentLocation = window.location.pathname + window.location.search;
// Redirect to login with state
window.location.href = `/login?expired=true&from=${encodeURIComponent(currentLocation)}`;
// Redirect to login with state (only show expired when a JWT existed)
let hadStoredJwt = false;
try {
hadStoredJwt = Boolean(localStorage.getItem('stirling_jwt'));
} catch {
// ignore storage access failures
}
const expiredPrefix = hadStoredJwt ? 'expired=true&' : '';
window.location.href = `/login?${expiredPrefix}from=${encodeURIComponent(currentLocation)}`;
return true; // Suppress toast since we're redirecting
}
@@ -173,4 +181,4 @@ export async function handleHttpError(error: any): Promise<boolean> {
}
return false; // Error was handled with toast, continue normal rejection
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { createClient, SupabaseClient } from '@supabase/supabase-js';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://rficokptxxxxtyzcvgmx.supabase.co';
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb';
// Check if Supabase is configured
export const isSupabaseConfigured = !!(supabaseUrl && supabaseAnonKey);
+4 -4
View File
@@ -101,8 +101,8 @@ export class UpdateService {
async getUpdateSummary(currentVersion: string, machineInfo: MachineInfo): Promise<UpdateSummary | null> {
// Map Java License enum to API types
let type = 'normal';
if (machineInfo.licenseType === 'PRO') {
type = 'pro';
if (machineInfo.licenseType === 'SERVER') {
type = 'server';
} else if (machineInfo.licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
@@ -133,8 +133,8 @@ export class UpdateService {
async getFullUpdateInfo(currentVersion: string, machineInfo: MachineInfo): Promise<FullUpdateInfo | null> {
// Map Java License enum to API types
let type = 'normal';
if (machineInfo.licenseType === 'PRO') {
type = 'pro';
if (machineInfo.licenseType === 'SERVER') {
type = 'server';
} else if (machineInfo.licenseType === 'ENTERPRISE') {
type = 'enterprise';
}
+12
View File
@@ -12,6 +12,12 @@
/* Inline highlights in summary */
--spdf-compare-inline-removed-bg: rgba(255, 59, 48, 0.25);
--spdf-compare-inline-added-bg: rgba(52, 199, 89, 0.25);
/* File Manager active file colors */
--file-active-bg: rgba(59, 130, 246, 0.1); /* Transparent blue for active file background */
--file-active-badge-bg: rgba(34, 197, 94, 0.15); /* Transparent green for active badge */
--file-active-badge-fg: #15803d; /* Green text for active badge */
--file-active-badge-border: rgba(34, 197, 94, 0.3); /* Green border for active badge */
}
/* CSS variables for Tailwind + Mantine integration */
@@ -158,6 +164,9 @@
--text-primary: #111827;
--text-secondary: #4b5563;
--text-muted: #6b7280;
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
--text-always-dark: #1f2937;
--text-always-dark-muted: #6b7280;
--border-subtle: #e5e7eb;
--border-default: #E2E8F0;
--border-strong: #9ca3af;
@@ -434,6 +443,9 @@
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
--text-always-dark: #1f2937;
--text-always-dark-muted: #6b7280;
--border-subtle: #2A2F36;
--border-default: #3A4047;
--border-strong: #4b5563;
+87
View File
@@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest';
import { isPdfFile, detectFileExtension, formatFileSize } from '@app/utils/fileUtils';
describe('fileUtils', () => {
describe('isPdfFile', () => {
it('should return true for PDF files with correct MIME type', () => {
const pdfFile = new File(['content'], 'document.pdf', { type: 'application/pdf' });
expect(isPdfFile(pdfFile)).toBe(true);
});
it('should return true for PDF files with .pdf extension even without MIME type', () => {
const pdfFile = new File(['content'], 'document.pdf', { type: '' });
expect(isPdfFile(pdfFile)).toBe(true);
});
it('should return false for non-PDF files', () => {
const txtFile = new File(['content'], 'document.txt', { type: 'text/plain' });
expect(isPdfFile(txtFile)).toBe(false);
});
it('should return false for image files', () => {
const imageFile = new File(['content'], 'image.png', { type: 'image/png' });
expect(isPdfFile(imageFile)).toBe(false);
});
it('should return false for null', () => {
expect(isPdfFile(null)).toBe(false);
});
it('should return false for undefined', () => {
expect(isPdfFile(undefined)).toBe(false);
});
it('should handle file-like objects with name and type', () => {
const fileLike = { name: 'test.pdf', type: 'application/pdf' };
expect(isPdfFile(fileLike)).toBe(true);
});
it('should handle file-like objects with PDF extension but no type', () => {
const fileLike = { name: 'test.pdf', type: '' };
expect(isPdfFile(fileLike)).toBe(true);
});
});
describe('detectFileExtension', () => {
it('should detect PDF extension', () => {
expect(detectFileExtension('document.pdf')).toBe('pdf');
});
it('should detect extension in uppercase', () => {
expect(detectFileExtension('document.PDF')).toBe('pdf');
});
it('should return empty string for files without extension', () => {
expect(detectFileExtension('document')).toBe('');
});
it('should handle multiple dots in filename', () => {
expect(detectFileExtension('my.document.pdf')).toBe('pdf');
});
it('should normalize jpeg to jpg', () => {
expect(detectFileExtension('image.jpeg')).toBe('jpg');
});
});
describe('formatFileSize', () => {
it('should format bytes', () => {
expect(formatFileSize(0)).toBe('0 B');
expect(formatFileSize(500)).toBe('500 B');
});
it('should format kilobytes', () => {
expect(formatFileSize(1024)).toBe('1 KB');
expect(formatFileSize(2048)).toBe('2 KB');
});
it('should format megabytes', () => {
expect(formatFileSize(1024 * 1024)).toBe('1 MB');
expect(formatFileSize(5 * 1024 * 1024)).toBe('5 MB');
});
it('should format gigabytes', () => {
expect(formatFileSize(1024 * 1024 * 1024)).toBe('1 GB');
});
});
});
+23
View File
@@ -51,3 +51,26 @@ export function detectFileExtension(filename: string): string {
return extension;
}
/**
* Checks if a file is a PDF based on extension and MIME type
* @param file - File or file-like object with name and type properties
* @returns true if the file appears to be a PDF
*/
export function isPdfFile(file: { name?: string; type?: string } | File | Blob | null | undefined): boolean {
if (!file) return false;
const name = 'name' in file ? file.name : undefined;
const type = file.type;
// Check MIME type first (most reliable)
if (type === 'application/pdf') return true;
// Check file extension as fallback
if (name) {
const ext = detectFileExtension(name);
if (ext === 'pdf') return true;
}
return false;
}
@@ -0,0 +1,390 @@
/**
* Desktop override of useInitialOnboardingState.
*
* Key difference: Handles the simplified onboarding flow where desktop-install
* and security-check slides are removed. When welcome is the last slide,
* clicking Next will launch the tools tour instead of doing nothing.
*/
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { useOnboarding } from '@app/contexts/OnboardingContext';
import { useOs } from '@app/hooks/useOs';
import { useNavigate } from 'react-router-dom';
import {
SLIDE_DEFINITIONS,
type ButtonAction,
type FlowState,
type SlideId,
} from '@app/components/onboarding/onboardingFlowConfig';
import type { LicenseNotice } from '@app/types/types';
import { resolveFlow } from '@app/components/onboarding/InitialOnboardingModal/flowResolver';
import { useServerExperience } from '@app/hooks/useServerExperience';
import { DEFAULT_STATE, type InitialOnboardingModalProps, type OnboardingState } from '@app/components/onboarding/InitialOnboardingModal/types';
import { DOWNLOAD_URLS } from '@app/constants/downloads';
interface UseInitialOnboardingStateResult {
state: OnboardingState;
totalSteps: number;
slideDefinition: (typeof SLIDE_DEFINITIONS)[SlideId];
currentSlide: ReturnType<(typeof SLIDE_DEFINITIONS)[SlideId]['createSlide']>;
licenseNotice: LicenseNotice;
flowState: FlowState;
closeAndMarkSeen: () => void;
handleButtonAction: (action: ButtonAction) => void;
}
export function useInitialOnboardingState({
opened,
onClose,
onRequestServerLicense,
onLicenseNoticeUpdate,
}: InitialOnboardingModalProps): UseInitialOnboardingStateResult | null {
const { preferences, updatePreference } = usePreferences();
const { startTour } = useOnboarding();
const {
loginEnabled: loginEnabledFromServer,
configIsAdmin,
totalUsers: serverTotalUsers,
userCountResolved: serverUserCountResolved,
freeTierLimit,
hasPaidLicense,
scenarioKey,
setSelfReportedAdmin,
isNewServer,
} = useServerExperience();
const osType = useOs();
const navigate = useNavigate();
const selectedDownloadUrlRef = useRef<string>('');
const [state, setState] = useState<OnboardingState>(DEFAULT_STATE);
const resetState = useCallback(() => {
setState(DEFAULT_STATE);
}, []);
useEffect(() => {
if (!opened) {
resetState();
}
}, [opened, resetState]);
const handleRoleSelect = useCallback(
(role: 'admin' | 'user' | null) => {
const isAdminSelection = role === 'admin';
setState((prev) => ({
...prev,
selectedRole: role,
selfReportedAdmin: isAdminSelection,
}));
if (typeof window !== 'undefined') {
if (isAdminSelection) {
window.localStorage.setItem('stirling-self-reported-admin', 'true');
} else {
window.localStorage.removeItem('stirling-self-reported-admin');
}
}
setSelfReportedAdmin(isAdminSelection);
},
[setSelfReportedAdmin],
);
const closeAndMarkSeen = useCallback(() => {
if (!preferences.hasSeenIntroOnboarding) {
updatePreference('hasSeenIntroOnboarding', true);
}
onClose();
}, [onClose, preferences.hasSeenIntroOnboarding, updatePreference]);
const isAdmin = configIsAdmin;
const enableLogin = loginEnabledFromServer;
const effectiveEnableLogin = enableLogin;
const effectiveIsAdmin = isAdmin;
const shouldAssumeAdminForNewServer = Boolean(isNewServer) && !effectiveEnableLogin;
useEffect(() => {
if (shouldAssumeAdminForNewServer && !state.selfReportedAdmin) {
handleRoleSelect('admin');
}
}, [handleRoleSelect, shouldAssumeAdminForNewServer, state.selfReportedAdmin]);
const shouldUseServerCount =
(effectiveEnableLogin && effectiveIsAdmin) || !effectiveEnableLogin;
const licenseUserCountFromServer =
shouldUseServerCount && serverUserCountResolved ? serverTotalUsers : null;
const effectiveLicenseUserCount = licenseUserCountFromServer ?? null;
const os = useMemo(() => {
switch (osType) {
case 'windows':
return { label: 'Windows', url: DOWNLOAD_URLS.WINDOWS };
case 'mac-apple':
return { label: 'Mac (Apple Silicon)', url: DOWNLOAD_URLS.MAC_APPLE_SILICON };
case 'mac-intel':
return { label: 'Mac (Intel)', url: DOWNLOAD_URLS.MAC_INTEL };
case 'linux-x64':
case 'linux-arm64':
return { label: 'Linux', url: DOWNLOAD_URLS.LINUX_DOCS };
default:
return { label: '', url: '' };
}
}, [osType]);
const osOptions = useMemo(() => {
const options = [
{ label: 'Windows', url: DOWNLOAD_URLS.WINDOWS, value: 'windows' },
{ label: 'Mac (Apple Silicon)', url: DOWNLOAD_URLS.MAC_APPLE_SILICON, value: 'mac-apple' },
{ label: 'Mac (Intel)', url: DOWNLOAD_URLS.MAC_INTEL, value: 'mac-intel' },
{ label: 'Linux', url: DOWNLOAD_URLS.LINUX_DOCS, value: 'linux' },
];
return options.filter(opt => opt.url);
}, []);
const resolvedFlow = useMemo(
() => resolveFlow(effectiveEnableLogin, effectiveIsAdmin, state.selfReportedAdmin),
[effectiveEnableLogin, effectiveIsAdmin, state.selfReportedAdmin],
);
// Desktop: No security-check slide, so no need to filter it out
const flowSlideIds = resolvedFlow.ids;
const flowType = resolvedFlow.type;
const totalSteps = flowSlideIds.length;
const maxIndex = Math.max(totalSteps - 1, 0);
useEffect(() => {
if (state.step >= flowSlideIds.length) {
setState((prev) => ({
...prev,
step: Math.max(flowSlideIds.length - 1, 0),
}));
}
}, [flowSlideIds.length, state.step]);
const currentSlideId = flowSlideIds[state.step] ?? flowSlideIds[flowSlideIds.length - 1];
const slideDefinition = SLIDE_DEFINITIONS[currentSlideId];
if (!slideDefinition) {
return null;
}
const scenarioProvidesInfo =
scenarioKey && scenarioKey !== 'unknown' && scenarioKey !== 'licensed';
const scenarioIndicatesAdmin = scenarioProvidesInfo
? scenarioKey!.includes('admin')
: state.selfReportedAdmin || effectiveIsAdmin;
const scenarioIndicatesOverLimit = scenarioProvidesInfo
? scenarioKey!.includes('over-limit')
: effectiveLicenseUserCount != null && effectiveLicenseUserCount > freeTierLimit;
const scenarioRequiresLicense =
scenarioKey === 'licensed' ? false : scenarioKey === 'unknown' ? !hasPaidLicense : true;
const shouldShowServerLicenseInfo = scenarioIndicatesAdmin && scenarioRequiresLicense;
const licenseNotice = useMemo<LicenseNotice>(
() => ({
totalUsers: effectiveLicenseUserCount,
freeTierLimit,
isOverLimit: scenarioIndicatesOverLimit,
requiresLicense: shouldShowServerLicenseInfo,
}),
[
effectiveLicenseUserCount,
freeTierLimit,
scenarioIndicatesOverLimit,
shouldShowServerLicenseInfo,
],
);
const requestServerLicenseIfNeeded = useCallback(
(options?: { deferUntilTourComplete?: boolean; selfReportedAdmin?: boolean }) => {
if (!shouldShowServerLicenseInfo) {
return;
}
onRequestServerLicense?.(options);
},
[onRequestServerLicense, shouldShowServerLicenseInfo],
);
useEffect(() => {
onLicenseNoticeUpdate?.(licenseNotice);
}, [licenseNotice, onLicenseNoticeUpdate]);
// Initialize ref with default URL
useEffect(() => {
if (!selectedDownloadUrlRef.current && os.url) {
selectedDownloadUrlRef.current = os.url;
}
}, [os.url]);
const handleDownloadUrlChange = useCallback((url: string) => {
selectedDownloadUrlRef.current = url;
}, []);
const currentSlide = slideDefinition.createSlide({
osLabel: os.label,
osUrl: os.url,
osOptions,
onDownloadUrlChange: handleDownloadUrlChange,
selectedRole: state.selectedRole,
onRoleSelect: handleRoleSelect,
licenseNotice,
loginEnabled: effectiveEnableLogin,
});
const goNext = useCallback(() => {
setState((prev) => ({
...prev,
step: Math.min(prev.step + 1, maxIndex),
}));
}, [maxIndex]);
const goPrev = useCallback(() => {
setState((prev) => ({
...prev,
step: Math.max(prev.step - 1, 0),
}));
}, []);
const launchTour = useCallback(
(mode: 'admin' | 'tools', options?: { closeOnboardingSlides?: boolean }) => {
if (options?.closeOnboardingSlides) {
closeAndMarkSeen();
}
startTour(mode, {
source: 'initial-onboarding-modal',
metadata: {
hasCompletedOnboarding: preferences.hasCompletedOnboarding,
toolPanelModePromptSeen: preferences.toolPanelModePromptSeen,
selfReportedAdmin: state.selfReportedAdmin,
},
});
},
[closeAndMarkSeen, preferences.hasCompletedOnboarding, preferences.toolPanelModePromptSeen, startTour, state.selfReportedAdmin],
);
const handleButtonAction = useCallback(
(action: ButtonAction) => {
const isOnLastSlide = state.step >= maxIndex;
// Desktop: For login-user and no-login flows, when on the last slide (welcome),
// clicking Next should launch the tools tour
const shouldAutoLaunchToolsTour =
(flowType === 'login-user' || flowType === 'no-login') && isOnLastSlide;
switch (action) {
case 'next':
if (shouldAutoLaunchToolsTour) {
launchTour('tools', { closeOnboardingSlides: true });
return;
}
goNext();
return;
case 'prev':
goPrev();
return;
case 'close':
closeAndMarkSeen();
return;
case 'download-selected': {
const downloadUrl = selectedDownloadUrlRef.current || os.url || currentSlide.downloadUrl;
if (downloadUrl) {
window.open(downloadUrl, '_blank', 'noopener');
}
if (shouldAutoLaunchToolsTour) {
launchTour('tools', { closeOnboardingSlides: true });
return;
}
goNext();
return;
}
case 'complete-close':
updatePreference('hasCompletedOnboarding', true);
closeAndMarkSeen();
return;
case 'security-next':
// Desktop: security-check slide is removed, but keep this for completeness
if (!state.selectedRole) {
return;
}
if (state.selectedRole === 'admin') {
goNext();
} else {
launchTour('tools', { closeOnboardingSlides: true });
}
return;
case 'launch-admin':
requestServerLicenseIfNeeded({
deferUntilTourComplete: true,
selfReportedAdmin: state.selfReportedAdmin || effectiveIsAdmin,
});
launchTour('admin', { closeOnboardingSlides: true });
return;
case 'launch-tools':
launchTour('tools', { closeOnboardingSlides: true });
return;
case 'launch-auto': {
const launchMode = state.selfReportedAdmin || effectiveIsAdmin ? 'admin' : 'tools';
if (launchMode === 'admin') {
requestServerLicenseIfNeeded({
deferUntilTourComplete: true,
selfReportedAdmin: state.selfReportedAdmin || effectiveIsAdmin,
});
}
launchTour(launchMode, { closeOnboardingSlides: true });
return;
}
case 'skip-to-license':
updatePreference('hasCompletedOnboarding', true);
requestServerLicenseIfNeeded({
deferUntilTourComplete: false,
selfReportedAdmin: state.selfReportedAdmin || effectiveIsAdmin,
});
closeAndMarkSeen();
return;
case 'see-plans':
closeAndMarkSeen();
navigate('/settings/adminPlan');
return;
default:
return;
}
},
[
closeAndMarkSeen,
currentSlide,
currentSlideId,
effectiveIsAdmin,
flowType,
goNext,
goPrev,
launchTour,
maxIndex,
navigate,
requestServerLicenseIfNeeded,
onRequestServerLicense,
os.url,
state.selectedRole,
state.selfReportedAdmin,
state.step,
updatePreference,
],
);
const flowState: FlowState = { selectedRole: state.selectedRole };
return {
state,
totalSteps,
slideDefinition,
currentSlide,
licenseNotice,
flowState,
closeAndMarkSeen,
handleButtonAction,
};
}
@@ -0,0 +1,178 @@
/**
* Desktop override of onboarding flow config.
*
* This version removes the desktop-install and security-check slides
* since they're not relevant when running as a desktop app.
*
* The SlideId type still includes all values for type compatibility,
* but the actual FLOW_SEQUENCES don't use these slides.
*/
import WelcomeSlide from '@app/components/onboarding/slides/WelcomeSlide';
import PlanOverviewSlide from '@app/components/onboarding/slides/PlanOverviewSlide';
import ServerLicenseSlide from '@app/components/onboarding/slides/ServerLicenseSlide';
import { SlideConfig, LicenseNotice } from '@app/types/types';
// Keep the full type for compatibility, but these slides won't be used
export type SlideId =
| 'welcome'
| 'desktop-install'
| 'security-check'
| 'admin-overview'
| 'server-license';
export type HeroType = 'rocket' | 'dual-icon' | 'shield' | 'diamond' | 'logo';
export type ButtonAction =
| 'next'
| 'prev'
| 'close'
| 'complete-close'
| 'download-selected'
| 'security-next'
| 'launch-admin'
| 'launch-tools'
| 'launch-auto'
| 'see-plans'
| 'skip-to-license';
export interface FlowState {
selectedRole: 'admin' | 'user' | null;
}
export interface OSOption {
label: string;
url: string;
value: string;
}
export interface SlideFactoryParams {
osLabel: string;
osUrl: string;
osOptions?: OSOption[];
onDownloadUrlChange?: (url: string) => void;
selectedRole: 'admin' | 'user' | null;
onRoleSelect: (role: 'admin' | 'user' | null) => void;
licenseNotice?: LicenseNotice;
loginEnabled?: boolean;
}
export interface HeroDefinition {
type: HeroType;
}
export interface ButtonDefinition {
key: string;
type: 'button' | 'icon';
label?: string;
icon?: 'chevron-left';
variant?: 'primary' | 'secondary' | 'default';
group: 'left' | 'right';
action: ButtonAction;
disabledWhen?: (state: FlowState) => boolean;
}
export interface SlideDefinition {
id: SlideId;
createSlide: (params: SlideFactoryParams) => SlideConfig;
hero: HeroDefinition;
buttons: ButtonDefinition[];
}
export const SLIDE_DEFINITIONS: Record<SlideId, SlideDefinition> = {
'welcome': {
id: 'welcome',
createSlide: () => WelcomeSlide(),
hero: { type: 'rocket' },
buttons: [
{
key: 'welcome-next',
type: 'button',
label: 'onboarding.buttons.next',
variant: 'primary',
group: 'right',
action: 'next',
},
],
},
// Stub definitions for desktop-install and security-check - not used on desktop
// but kept for type compatibility with core code
'desktop-install': {
id: 'desktop-install',
createSlide: () => WelcomeSlide(), // Placeholder - never used
hero: { type: 'dual-icon' },
buttons: [],
},
'security-check': {
id: 'security-check',
createSlide: () => WelcomeSlide(), // Placeholder - never used
hero: { type: 'shield' },
buttons: [],
},
'admin-overview': {
id: 'admin-overview',
createSlide: ({ licenseNotice, loginEnabled }) =>
PlanOverviewSlide({ isAdmin: true, licenseNotice, loginEnabled }),
hero: { type: 'diamond' },
buttons: [
{
key: 'admin-back',
type: 'icon',
icon: 'chevron-left',
group: 'left',
action: 'prev',
},
{
key: 'admin-show',
type: 'button',
label: 'onboarding.buttons.showMeAround',
variant: 'primary',
group: 'right',
action: 'launch-admin',
},
{
key: 'admin-skip',
type: 'button',
label: 'onboarding.buttons.skipTheTour',
variant: 'secondary',
group: 'left',
action: 'skip-to-license',
},
],
},
'server-license': {
id: 'server-license',
createSlide: ({ licenseNotice }) => ServerLicenseSlide({ licenseNotice }),
hero: { type: 'dual-icon' },
buttons: [
{
key: 'license-close',
type: 'button',
label: 'onboarding.buttons.skipForNow',
variant: 'secondary',
group: 'left',
action: 'close',
},
{
key: 'license-see-plans',
type: 'button',
label: 'onboarding.serverLicense.seePlans',
variant: 'primary',
group: 'right',
action: 'see-plans',
},
],
},
};
/**
* Desktop flow sequences - simplified without desktop-install and security-check slides
* since users are already on desktop and security check is not needed.
*/
export const FLOW_SEQUENCES = {
loginAdmin: ['welcome', 'admin-overview'] as SlideId[],
loginUser: ['welcome'] as SlideId[],
noLoginBase: ['welcome'] as SlideId[],
noLoginAdmin: ['admin-overview'] as SlideId[],
};
@@ -1,8 +1,39 @@
import { createConfigNavSections as createProprietaryConfigNavSections } from '@proprietary/components/shared/config/configNavSections';
import { useTranslation } from 'react-i18next';
import { useConfigNavSections as useProprietaryConfigNavSections, createConfigNavSections as createProprietaryConfigNavSections } from '@proprietary/components/shared/config/configNavSections';
import { ConfigNavSection } from '@core/components/shared/config/configNavSections';
import { ConnectionSettings } from '@app/components/ConnectionSettings';
/**
* Hook version of desktop config nav sections with proper i18n support
*/
export const useConfigNavSections = (
isAdmin: boolean = false,
runningEE: boolean = false,
loginEnabled: boolean = false
): ConfigNavSection[] => {
const { t } = useTranslation();
// Get the proprietary sections (includes core Preferences + admin sections)
const sections = useProprietaryConfigNavSections(isAdmin, runningEE, loginEnabled);
// Add Connection section at the beginning (after Preferences)
sections.splice(1, 0, {
title: t('settings.connection.title', 'Connection Mode'),
items: [
{
key: 'connectionMode',
label: t('settings.connection.title', 'Connection Mode'),
icon: 'cloud-rounded',
component: <ConnectionSettings />,
},
],
});
return sections;
};
/**
* Deprecated: Use useConfigNavSections hook instead
* Desktop extension of createConfigNavSections that adds connection settings
*/
export const createConfigNavSections = (
@@ -10,6 +41,8 @@ export const createConfigNavSections = (
runningEE: boolean = false,
loginEnabled: boolean = false
): ConfigNavSection[] => {
console.warn('createConfigNavSections is deprecated. Use useConfigNavSections hook instead for proper i18n support.');
// Get the proprietary sections (includes core Preferences + admin sections)
const sections = createProprietaryConfigNavSections(isAdmin, runningEE, loginEnabled);
+1 -1
View File
@@ -8,4 +8,4 @@ export const STIRLING_SAAS_URL: string = import.meta.env.VITE_SAAS_SERVER_URL ||
// Supabase publishable key from environment variable
// Used for SaaS authentication
export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || '';
export const SUPABASE_KEY: string = import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY || 'sb_publishable_UHz2SVRF5mvdrPHWkRteyA_yNlZTkYb';
+2 -2
View File
@@ -33,11 +33,11 @@ export const useDefaultApp = () => {
body: t('defaultApp.success.message', 'Stirling PDF is now your default PDF editor'),
});
setIsDefault(true);
} else if (result === 'opened_settings') {
} else if (result === 'opened_dialog') {
alert({
alertType: 'neutral',
title: t('defaultApp.settingsOpened.title', 'Settings Opened'),
body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in your system settings'),
body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in the file association dialogue'),
});
}
} catch (error) {
@@ -88,8 +88,7 @@ export function useEndpointEnabled(endpoint: string): {
try {
setError(null);
const response = await apiClient.get<boolean>('/api/v1/config/endpoint-enabled', {
params: { endpoint },
const response = await apiClient.get<boolean>(`/api/v1/config/endpoint-enabled?endpoint=${encodeURIComponent(endpoint)}`, {
suppressErrorToast: true,
});
@@ -22,10 +22,10 @@ export const defaultAppService = {
* Set or prompt to set Stirling PDF as default PDF handler
* Returns a status string indicating what happened
*/
async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_settings' | 'error'> {
async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_dialog' | 'error'> {
try {
const result = await invoke<string>('set_as_default_pdf_handler');
return result as 'set_successfully' | 'opened_settings';
return result as 'set_successfully' | 'opened_dialog';
} catch (error) {
console.error('[DefaultApp] Failed to set default handler:', error);
return 'error';
+2 -2
View File
@@ -12,8 +12,8 @@ import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { BASE_PATH } from '@app/constants/app';
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
posthog.init('phc_VOdeYnlevc2T63m3myFGjeBlRcIusRgmhfx6XL5a1iz', {
api_host: 'https://eu.i.posthog.com',
defaults: '2025-05-24',
capture_exceptions: true, // This enables capturing exceptions using Error Tracking, set to false if you don't want this
debug: false,
@@ -2,9 +2,9 @@ import { AppProviders as CoreAppProviders, AppProvidersProps } from "@core/compo
import { AuthProvider } from "@app/auth/UseSession";
import { LicenseProvider } from "@app/contexts/LicenseContext";
import { CheckoutProvider } from "@app/contexts/CheckoutContext";
import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext"
import { UpgradeBannerInitializer } from "@app/components/shared/UpgradeBannerInitializer";
import { ServerExperienceProvider } from "@app/contexts/ServerExperienceContext";
import { UpdateSeatsProvider } from "@app/contexts/UpdateSeatsContext";
export function AppProviders({ children, appConfigRetryOptions, appConfigProviderProps }: AppProvidersProps) {
return (
@@ -1,5 +1,6 @@
import React from 'react';
import { createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
import { useTranslation } from 'react-i18next';
import { useConfigNavSections as useCoreConfigNavSections, createConfigNavSections as createCoreConfigNavSections, ConfigNavSection } from '@core/components/shared/config/configNavSections';
import PeopleSection from '@app/components/shared/config/configSections/PeopleSection';
import TeamsSection from '@app/components/shared/config/configSections/TeamsSection';
import AdminGeneralSection from '@app/components/shared/config/configSections/AdminGeneralSection';
@@ -17,6 +18,196 @@ import AdminUsageSection from '@app/components/shared/config/configSections/Admi
import ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
/**
* Hook version of proprietary config nav sections with proper i18n support
*/
export const useConfigNavSections = (
isAdmin: boolean = false,
runningEE: boolean = false,
loginEnabled: boolean = false
): ConfigNavSection[] => {
const { t } = useTranslation();
// Get the core sections (just Preferences)
const sections = useCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
// Add Admin sections if user is admin OR if login is disabled (but mark as disabled)
if (isAdmin || !loginEnabled) {
const requiresLogin = !loginEnabled;
const enableLoginTooltip = t('settings.tooltips.enableLoginFirst', 'Enable login mode first');
const requiresEnterpriseTooltip = t('settings.tooltips.requiresEnterprise', 'Requires Enterprise license');
// Workspace
sections.push({
title: t('settings.workspace.title', 'Workspace'),
items: [
{
key: 'people',
label: t('settings.workspace.people', 'People'),
icon: 'group-rounded',
component: <PeopleSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'teams',
label: t('settings.workspace.teams', 'Teams'),
icon: 'groups-rounded',
component: <TeamsSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
],
});
// Configuration
sections.push({
title: t('settings.configuration.title', 'Configuration'),
items: [
{
key: 'adminGeneral',
label: t('settings.configuration.systemSettings', 'System Settings'),
icon: 'settings-rounded',
component: <AdminGeneralSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminFeatures',
label: t('settings.configuration.features', 'Features'),
icon: 'extension-rounded',
component: <AdminFeaturesSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminEndpoints',
label: t('settings.configuration.endpoints', 'Endpoints'),
icon: 'api-rounded',
component: <AdminEndpointsSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminDatabase',
label: t('settings.configuration.database', 'Database'),
icon: 'storage-rounded',
component: <AdminDatabaseSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminAdvanced',
label: t('settings.configuration.advanced', 'Advanced'),
icon: 'tune-rounded',
component: <AdminAdvancedSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
],
});
// Security & Authentication
sections.push({
title: t('settings.securityAuth.title', 'Security & Authentication'),
items: [
{
key: 'adminSecurity',
label: t('settings.securityAuth.security', 'Security'),
icon: 'shield-rounded',
component: <AdminSecuritySection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminConnections',
label: t('settings.securityAuth.connections', 'Connections'),
icon: 'link-rounded',
component: <AdminConnectionsSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
],
});
// Licensing & Analytics
sections.push({
title: t('settings.licensingAnalytics.title', 'Licensing & Analytics'),
items: [
{
key: 'adminPlan',
label: t('settings.licensingAnalytics.plan', 'Plan'),
icon: 'star-rounded',
component: <AdminPlanSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminAudit',
label: t('settings.licensingAnalytics.audit', 'Audit'),
icon: 'fact-check-rounded',
component: <AdminAuditSection />,
disabled: !runningEE || requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : requiresEnterpriseTooltip
},
{
key: 'adminUsage',
label: t('settings.licensingAnalytics.usageAnalytics', 'Usage Analytics'),
icon: 'analytics-rounded',
component: <AdminUsageSection />,
disabled: !runningEE || requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : requiresEnterpriseTooltip
},
],
});
// Policies & Privacy
sections.push({
title: t('settings.policiesPrivacy.title', 'Policies & Privacy'),
items: [
{
key: 'adminLegal',
label: t('settings.policiesPrivacy.legal', 'Legal'),
icon: 'gavel-rounded',
component: <AdminLegalSection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
{
key: 'adminPrivacy',
label: t('settings.policiesPrivacy.privacy', 'Privacy'),
icon: 'visibility-rounded',
component: <AdminPrivacySection />,
disabled: requiresLogin,
disabledTooltip: requiresLogin ? enableLoginTooltip : undefined
},
],
});
}
// Add Developer section if login is enabled
if (loginEnabled) {
const developerSection: ConfigNavSection = {
title: t('settings.developer.title', 'Developer'),
items: [
{
key: 'api-keys',
label: t('settings.developer.apiKeys', 'API Keys'),
icon: 'key-rounded',
component: <ApiKeys />
},
],
};
// Add Developer section after Preferences (or Workspace if it exists)
const insertIndex = isAdmin ? 2 : 1;
sections.splice(insertIndex, 0, developerSection);
}
return sections;
};
/**
* Deprecated: Use useConfigNavSections hook instead
* Proprietary extension of createConfigNavSections that adds all admin and workspace sections
*/
export const createConfigNavSections = (
@@ -24,6 +215,8 @@ export const createConfigNavSections = (
runningEE: boolean = false,
loginEnabled: boolean = false
): ConfigNavSection[] => {
console.warn('createConfigNavSections is deprecated. Use useConfigNavSections hook instead for proper i18n support.');
// Get the core sections (just Preferences)
const sections = createCoreConfigNavSections(isAdmin, runningEE, loginEnabled);
@@ -54,6 +54,7 @@ export default function AdminGeneralSection() {
const [originalSettingsSnapshot, setOriginalSettingsSnapshot] = useState<string>('');
const [isDirty, setLocalIsDirty] = useState(false);
const isInitialLoad = useRef(true);
const justSavedRef = useRef(false);
const {
settings,
@@ -158,9 +159,12 @@ export default function AdminGeneralSection() {
}
}, [loginEnabled, fetchSettings]);
// Snapshot original settings after initial load and sync local preference with server
// Snapshot original settings after initial load OR after successful save (when refetch completes)
useEffect(() => {
if (!loading && isInitialLoad.current && Object.keys(settings).length > 0) {
if (loading || Object.keys(settings).length === 0) return;
// After initial load: set snapshot and sync preference
if (isInitialLoad.current) {
setOriginalSettingsSnapshot(JSON.stringify(settings));
// Sync local preference with server setting on initial load to ensure they're in sync
@@ -170,8 +174,17 @@ export default function AdminGeneralSection() {
}
isInitialLoad.current = false;
return;
}
}, [loading, settings, loginEnabled, updatePreference]);
// After save: update snapshot to new server state so dirty tracking is accurate
if (justSavedRef.current) {
setOriginalSettingsSnapshot(JSON.stringify(settings));
setLocalIsDirty(false);
setIsDirty(false);
justSavedRef.current = false;
}
}, [loading, settings, loginEnabled, updatePreference, setIsDirty]);
// Track dirty state by comparing current settings to snapshot
useEffect(() => {
@@ -238,6 +251,9 @@ export default function AdminGeneralSection() {
}
try {
// Mark that we just saved - the snapshot will be updated when refetch completes
justSavedRef.current = true;
await saveSettings();
// Update local preference after successful save so the app reflects the saved logo style
@@ -245,12 +261,12 @@ export default function AdminGeneralSection() {
updatePreference('logoVariant', settings.ui.logoStyle);
}
// Update snapshot to current settings after successful save
setOriginalSettingsSnapshot(JSON.stringify(settings));
// Clear dirty state immediately (snapshot will be updated by effect when refetch completes)
setLocalIsDirty(false);
markClean();
showRestartModal();
} catch (_error) {
justSavedRef.current = false;
alert({
alertType: 'error',
title: t('admin.error', 'Error'),
@@ -320,7 +336,7 @@ export default function AdminGeneralSection() {
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.25rem 0' }}>
<img
src="/classic-logo/favicon.ico"
alt="Classic logo"
alt={t('admin.settings.general.logoStyle.classicAlt', 'Classic logo')}
style={{ width: '24px', height: '24px' }}
/>
<span>{t('admin.settings.general.logoStyle.classic', 'Classic')}</span>
@@ -333,7 +349,7 @@ export default function AdminGeneralSection() {
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.25rem 0' }}>
<img
src="/modern-logo/StirlingPDFLogoNoTextLight.svg"
alt="Modern logo"
alt={t('admin.settings.general.logoStyle.modernAlt', 'Modern logo')}
style={{ width: '24px', height: '24px' }}
/>
<span>{t('admin.settings.general.logoStyle.modern', 'Modern')}</span>
@@ -369,7 +385,7 @@ export default function AdminGeneralSection() {
]}
searchable
clearable
placeholder="Select languages"
placeholder={t('admin.settings.general.languages.placeholder', 'Select languages')}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
@@ -222,6 +222,7 @@ const AdminPlanSection: React.FC = () => {
buttonColor="orange.7"
/>
)}
<AvailablePlansSection
plans={plans}
currentLicenseInfo={licenseInfo}

Some files were not shown because too many files have changed in this diff Show More