From 350fdcf29ac4fa2b63220f53e88e9c4532146500 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bal=C3=A1zs=20Sz=C3=BCcs?=
<127139797+balazs-szucs@users.noreply.github.com>
Date: Thu, 13 Nov 2025 16:57:14 +0100
Subject: [PATCH 1/4] [V2] feat(merge): implement natural sorting for filenames
in merge tool (#4888)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description of Changes
TLDR:
- Added `naturalCompare` function to handle alphanumeric sorting
- Updated `sortFiles` logic to use `naturalCompare` for filename sorting
- Passed `naturalCompare` as dependency in sorting callback
This pull request improves the file sorting logic in the `Merge` tool to
provide a more natural, human-friendly ordering of filenames (e.g.,
"file2" now comes before "file10" instead of after). The main change is
the introduction of a custom `naturalCompare` function that is used when
sorting files by filename.
File sorting improvements:
* Added a `naturalCompare` function to sort filenames in a way that
handles numeric portions naturally, ensuring files like "file2" are
ordered before "file10" (`frontend/src/core/tools/Merge.tsx`).
* Updated the file sorting logic to use `naturalCompare` instead of the
default `localeCompare` when sorting by filename
(`frontend/src/core/tools/Merge.tsx`).
* Ensured the `sortFiles` callback properly depends on the new
`naturalCompare` function (`frontend/src/core/tools/Merge.tsx`).
Note: the sort on upload is natural sort (at least I think so I haven't
checked the code), this is only relevant after upload, and you click the
sort button again
### Before:
### After:
---
## Checklist
### General
- [X] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [X] 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)
- [X] I have performed a self-review of my own code
- [X] 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)
- [X] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)
### Testing (if applicable)
- [X] 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.
---------
Signed-off-by: Balázs Szücs
---
frontend/src/core/tools/Merge.tsx | 55 +++++++++++++++++++++++++++++--
1 file changed, 53 insertions(+), 2 deletions(-)
diff --git a/frontend/src/core/tools/Merge.tsx b/frontend/src/core/tools/Merge.tsx
index e725291db..05d358f70 100644
--- a/frontend/src/core/tools/Merge.tsx
+++ b/frontend/src/core/tools/Merge.tsx
@@ -26,6 +26,57 @@ const Merge = (props: BaseToolProps) => {
props,
{ minFiles: 2 }
);
+ const naturalCompare = useCallback((a: string, b: string): number => {
+ const isDigit = (char: string) => char >= '0' && char <= '9';
+
+ const getChunk = (s: string, length: number, marker: number): { chunk: string; newMarker: number } => {
+ let chunk = '';
+ const c = s.charAt(marker);
+ chunk += c;
+ marker++;
+
+ if (isDigit(c)) {
+ while (marker < length && isDigit(s.charAt(marker))) {
+ chunk += s.charAt(marker);
+ marker++;
+ }
+ } else {
+ while (marker < length && !isDigit(s.charAt(marker))) {
+ chunk += s.charAt(marker);
+ marker++;
+ }
+ }
+ return { chunk, newMarker: marker };
+ };
+
+ const len1 = a.length;
+ const len2 = b.length;
+ let marker1 = 0;
+ let marker2 = 0;
+
+ while (marker1 < len1 && marker2 < len2) {
+ const { chunk: chunk1, newMarker: newMarker1 } = getChunk(a, len1, marker1);
+ marker1 = newMarker1;
+
+ const { chunk: chunk2, newMarker: newMarker2 } = getChunk(b, len2, marker2);
+ marker2 = newMarker2;
+
+ let result: number;
+ if (isDigit(chunk1.charAt(0)) && isDigit(chunk2.charAt(0))) {
+ const num1 = parseInt(chunk1, 10);
+ const num2 = parseInt(chunk2, 10);
+ result = num1 - num2;
+ } else {
+ result = chunk1.localeCompare(chunk2);
+ }
+
+ if (result !== 0) {
+ return result;
+ }
+ }
+
+ return len1 - len2;
+ }, []);
// Custom file sorting logic for merge tool
const sortFiles = useCallback((sortType: 'filename' | 'dateModified', ascending: boolean = true) => {
@@ -33,7 +84,7 @@ const Merge = (props: BaseToolProps) => {
let comparison = 0;
switch (sortType) {
case 'filename':
- comparison = stubA.name.localeCompare(stubB.name);
+ comparison = naturalCompare(stubA.name, stubB.name);
break;
case 'dateModified':
comparison = stubA.lastModified - stubB.lastModified;
@@ -45,7 +96,7 @@ const Merge = (props: BaseToolProps) => {
const selectedIds = sortedStubs.map(record => record.id);
const deselectedIds = fileIds.filter(id => !selectedIds.includes(id));
reorderFiles([...selectedIds, ...deselectedIds]);
- }, [selectedFileStubs, fileIds, reorderFiles]);
+ }, [selectedFileStubs, fileIds, reorderFiles, naturalCompare]);
return createToolFlow({
files: {
From 052a3ae653348ee0db7f766e611a856ae848a13e Mon Sep 17 00:00:00 2001
From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
Date: Thu, 13 Nov 2025 17:29:53 +0000
Subject: [PATCH 2/4] api (#4892)
# Description of Changes
---
## 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.
---
.../public/locales/en-GB/translation.json | 6 +
.../core/components/shared/AppConfigModal.tsx | 8 +-
.../shared/config/configNavSections.tsx | 3 +-
frontend/src/core/styles/theme.css | 18 +++
.../shared/config/configNavSections.tsx | 23 +++-
.../shared/config/configSections/ApiKeys.tsx | 126 ++++++++++++++++++
.../configSections/apiKeys/ApiKeySection.tsx | 75 +++++++++++
.../configSections/apiKeys/RefreshModal.tsx | 50 +++++++
.../configSections/apiKeys/hooks/useApiKey.ts | 64 +++++++++
9 files changed, 368 insertions(+), 5 deletions(-)
create mode 100644 frontend/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
create mode 100644 frontend/src/proprietary/components/shared/config/configSections/apiKeys/ApiKeySection.tsx
create mode 100644 frontend/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.tsx
create mode 100644 frontend/src/proprietary/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts
diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json
index 2665b3420..9cfe5380b 100644
--- a/frontend/public/locales/en-GB/translation.json
+++ b/frontend/public/locales/en-GB/translation.json
@@ -4650,6 +4650,12 @@
}
},
"apiKeys": {
+ "intro": "Use your API key to programmatically access Stirling PDF's processing capabilities.",
+ "docsTitle": "API Documentation",
+ "docsDescription": "Learn more about integrating with Stirling PDF:",
+ "docsLink": "API Documentation",
+ "schemaLink": "API Schema Reference",
+ "usage": "Include this key in the X-API-KEY header with all API requests.",
"description": "Your API key for accessing Stirling's suite of PDF tools. Copy it to your project or refresh to generate a new one.",
"publicKeyAriaLabel": "Public API key",
"copyKeyAriaLabel": "Copy API key",
diff --git a/frontend/src/core/components/shared/AppConfigModal.tsx b/frontend/src/core/components/shared/AppConfigModal.tsx
index e69aa97e8..f9937f775 100644
--- a/frontend/src/core/components/shared/AppConfigModal.tsx
+++ b/frontend/src/core/components/shared/AppConfigModal.tsx
@@ -64,17 +64,19 @@ const AppConfigModal: React.FC = ({ opened, onClose }) => {
headerBorder: 'var(--modal-header-border)',
}), []);
- // Get isAdmin and runningEE from app config
+ // Get isAdmin, runningEE, and loginEnabled from app config
const isAdmin = config?.isAdmin ?? false;
const runningEE = config?.runningEE ?? false;
+ const loginEnabled = config?.enableLogin ?? false;
// Left navigation structure and icons
const configNavSections = useMemo(() =>
createConfigNavSections(
isAdmin,
- runningEE
+ runningEE,
+ loginEnabled
),
- [isAdmin, runningEE]
+ [isAdmin, runningEE, loginEnabled]
);
const activeLabel = useMemo(() => {
diff --git a/frontend/src/core/components/shared/config/configNavSections.tsx b/frontend/src/core/components/shared/config/configNavSections.tsx
index cbef8d27e..a1f66480b 100644
--- a/frontend/src/core/components/shared/config/configNavSections.tsx
+++ b/frontend/src/core/components/shared/config/configNavSections.tsx
@@ -41,7 +41,8 @@ export interface ConfigColors {
export const createConfigNavSections = (
isAdmin: boolean = false,
- runningEE: boolean = false
+ runningEE: boolean = false,
+ _loginEnabled: boolean = false
): ConfigNavSection[] => {
const sections: ConfigNavSection[] = [
{
diff --git a/frontend/src/core/styles/theme.css b/frontend/src/core/styles/theme.css
index 3051ced06..5e8d4087b 100644
--- a/frontend/src/core/styles/theme.css
+++ b/frontend/src/core/styles/theme.css
@@ -279,6 +279,15 @@
--modal-content-bg: #ffffff;
--modal-header-border: rgba(0, 0, 0, 0.06);
+ /* API Keys section colors (light mode) */
+ --api-keys-card-bg: #ffffff;
+ --api-keys-card-border: #e0e0e0;
+ --api-keys-card-shadow: rgba(0, 0, 0, 0.06);
+ --api-keys-input-bg: #f8f8f8;
+ --api-keys-input-border: #e0e0e0;
+ --api-keys-button-bg: #f5f5f5;
+ --api-keys-button-color: #333333;
+
/* PDF Report Colors (always light) */
--pdf-light-header-bg: 239 246 255;
--pdf-light-accent: 59 130 246;
@@ -539,6 +548,15 @@
--modal-content-bg: #2A2F36;
--modal-header-border: rgba(255, 255, 255, 0.08);
+ /* API Keys section colors (dark mode) */
+ --api-keys-card-bg: #2A2F36;
+ --api-keys-card-border: #3A4047;
+ --api-keys-card-shadow: none;
+ --api-keys-input-bg: #1F2329;
+ --api-keys-input-border: #3A4047;
+ --api-keys-button-bg: #3A4047;
+ --api-keys-button-color: #D0D6DC;
+
/* Code token colors (dark mode - Cursor-like) */
--code-kw-color: #C792EA; /* purple */
--code-str-color: #C3E88D; /* green */
diff --git a/frontend/src/proprietary/components/shared/config/configNavSections.tsx b/frontend/src/proprietary/components/shared/config/configNavSections.tsx
index 1df66b409..9be7640cc 100644
--- a/frontend/src/proprietary/components/shared/config/configNavSections.tsx
+++ b/frontend/src/proprietary/components/shared/config/configNavSections.tsx
@@ -2,13 +2,15 @@ import React from 'react';
import { 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 ApiKeys from '@app/components/shared/config/configSections/ApiKeys';
/**
* Proprietary extension of createConfigNavSections that adds workspace sections
*/
export const createConfigNavSections = (
isAdmin: boolean = false,
- runningEE: boolean = false
+ runningEE: boolean = false,
+ loginEnabled: boolean = false
): ConfigNavSection[] => {
// Get the core sections
const sections = createCoreConfigNavSections(isAdmin, runningEE);
@@ -37,6 +39,25 @@ export const createConfigNavSections = (
sections.splice(1, 0, workspaceSection);
}
+ // Add Developer section if login is enabled
+ if (loginEnabled) {
+ const developerSection: ConfigNavSection = {
+ title: 'Developer',
+ items: [
+ {
+ key: 'api-keys',
+ label: 'API Keys',
+ icon: 'key-rounded',
+ component:
+ },
+ ],
+ };
+
+ // Add Developer section after Preferences (or Workspace if it exists)
+ const insertIndex = isAdmin ? 2 : 1;
+ sections.splice(insertIndex, 0, developerSection);
+ }
+
return sections;
};
diff --git a/frontend/src/proprietary/components/shared/config/configSections/ApiKeys.tsx b/frontend/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
new file mode 100644
index 000000000..fcc910887
--- /dev/null
+++ b/frontend/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
@@ -0,0 +1,126 @@
+import React, { useState } from "react";
+import { Anchor, Group, Stack, Text, Paper, Skeleton } from "@mantine/core";
+// eslint-disable-next-line no-restricted-imports
+import ApiKeySection from "./apiKeys/ApiKeySection";
+// eslint-disable-next-line no-restricted-imports
+import RefreshModal from "./apiKeys/RefreshModal";
+// eslint-disable-next-line no-restricted-imports
+import useApiKey from "./apiKeys/hooks/useApiKey";
+import { useTranslation } from "react-i18next";
+import LocalIcon from "@app/components/shared/LocalIcon";
+
+export default function ApiKeys() {
+ const [copied, setCopied] = useState(null);
+ const [showRefreshModal, setShowRefreshModal] = useState(false);
+ const { t } = useTranslation();
+
+ const { apiKey, isLoading: apiKeyLoading, refresh, isRefreshing, error: apiKeyError, refetch } = useApiKey();
+
+ const copy = async (text: string, tag: string) => {
+ try {
+ await navigator.clipboard.writeText(text);
+ setCopied(tag);
+ setTimeout(() => setCopied(null), 1600);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ const refreshKeys = async () => {
+ try {
+ await refresh();
+ } finally {
+ setShowRefreshModal(false);
+ }
+ };
+
+ return (
+
+
+ {t('config.apiKeys.intro', 'Use your API key to programmatically access Stirling PDF\'s processing capabilities.')}
+
+
+
+
+
+
+
+ {t('config.apiKeys.docsTitle', 'API Documentation')}
+
+
+ {t('config.apiKeys.docsDescription', 'Learn more about integrating with Stirling PDF:')}
+
+
+
+
+ {t('config.apiKeys.docsLink', 'API Documentation')}
+
+
+
+
+
+ {t('config.apiKeys.schemaLink', 'API Schema Reference')}
+
+
+
+
+
+
+
+
+ {apiKeyError && (
+
+ {t('config.apiKeys.generateError', "We couldn't generate your API key.")} {" "}
+
+ {t('common.retry', 'Retry')}
+
+
+ )}
+
+ {apiKeyLoading ? (
+
+
+
+
+
+
+
+ ) : (
+ setShowRefreshModal(true)}
+ disabled={isRefreshing}
+ />
+ )}
+
+
+ {t('config.apiKeys.usage', 'Include this key in the X-API-KEY header with all API requests.')}
+
+
+ setShowRefreshModal(false)}
+ onConfirm={refreshKeys}
+ />
+
+ );
+}
diff --git a/frontend/src/proprietary/components/shared/config/configSections/apiKeys/ApiKeySection.tsx b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/ApiKeySection.tsx
new file mode 100644
index 000000000..e258dc6dc
--- /dev/null
+++ b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/ApiKeySection.tsx
@@ -0,0 +1,75 @@
+import React from "react";
+import {
+ Box,
+ Button,
+ Group,
+ Paper,
+} from "@mantine/core";
+import LocalIcon from "@app/components/shared/LocalIcon";
+import FitText from "@app/components/shared/FitText";
+import { useTranslation } from "react-i18next";
+
+interface ApiKeySectionProps {
+ publicKey: string;
+ copied: string | null;
+ onCopy: (text: string, tag: string) => void;
+ onRefresh: () => void;
+ disabled?: boolean;
+}
+
+export default function ApiKeySection({
+ publicKey,
+ copied,
+ onCopy,
+ onRefresh,
+ disabled,
+}: ApiKeySectionProps) {
+ const { t } = useTranslation();
+ return (
+ <>
+
+
+
+
+
+
+
+
+ }
+ styles={{ root: { background: "var(--api-keys-button-bg)", color: "var(--api-keys-button-color)", border: "none", marginLeft: 8 } }}
+ disabled={disabled}
+ aria-label={t('config.apiKeys.refreshAriaLabel', 'Refresh API key')}
+ >
+ {t('common.refresh', 'Refresh')}
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.tsx b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.tsx
new file mode 100644
index 000000000..faa6e60b4
--- /dev/null
+++ b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/RefreshModal.tsx
@@ -0,0 +1,50 @@
+import React from "react";
+import {
+ Modal,
+ Stack,
+ Text,
+ Group,
+ Button,
+} from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
+
+interface RefreshModalProps {
+ opened: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+}
+
+export default function RefreshModal({ opened, onClose, onConfirm }: RefreshModalProps) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {t('config.apiKeys.refreshModal.warning', '⚠️ Warning: This action will generate new API keys and make your previous keys invalid.')}
+
+
+ {t('config.apiKeys.refreshModal.impact', 'Any applications or services currently using these keys will stop working until you update them with the new keys.')}
+
+
+ {t('config.apiKeys.refreshModal.confirmPrompt', 'Are you sure you want to continue?')}
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/proprietary/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts
new file mode 100644
index 000000000..c8efe73f4
--- /dev/null
+++ b/frontend/src/proprietary/components/shared/config/configSections/apiKeys/hooks/useApiKey.ts
@@ -0,0 +1,64 @@
+import { useCallback, useEffect, useState } from "react";
+import apiClient from "@app/services/apiClient";
+
+export function useApiKey() {
+ const [apiKey, setApiKey] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+ const [hasAttempted, setHasAttempted] = useState(false);
+
+ const fetchKey = useCallback(async () => {
+ setIsLoading(true);
+ setError(null);
+ try {
+ // Backend is POST for get and update
+ const res = await apiClient.post("/api/v1/user/get-api-key");
+ const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
+ if (typeof value === "string") setApiKey(value);
+ } catch (e: any) {
+ // If not found, try to create one by calling update endpoint
+ if (e?.response?.status === 404) {
+ try {
+ const createRes = await apiClient.post("/api/v1/user/update-api-key");
+ const created =
+ typeof createRes.data === "string"
+ ? createRes.data
+ : createRes.data?.apiKey;
+ if (typeof created === "string") setApiKey(created);
+ } catch (createErr: any) {
+ setError(createErr);
+ }
+ } else {
+ setError(e);
+ }
+ } finally {
+ setIsLoading(false);
+ setHasAttempted(true);
+ }
+ }, []);
+
+ const refresh = useCallback(async () => {
+ setIsRefreshing(true);
+ setError(null);
+ try {
+ const res = await apiClient.post("/api/v1/user/update-api-key");
+ const value = typeof res.data === "string" ? res.data : res.data?.apiKey;
+ if (typeof value === "string") setApiKey(value);
+ } catch (e: any) {
+ setError(e);
+ } finally {
+ setIsRefreshing(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!hasAttempted) {
+ fetchKey();
+ }
+ }, [hasAttempted, fetchKey]);
+
+ return { apiKey, isLoading, isRefreshing, error, refetch: fetchKey, refresh, hasAttempted } as const;
+}
+
+export default useApiKey;
From edba81ff9dd1dc4c2cd9f3f1ba3bb146ad3fd710 Mon Sep 17 00:00:00 2001
From: "stirlingbot[bot]" <195170888+stirlingbot[bot]@users.noreply.github.com>
Date: Thu, 13 Nov 2025 17:46:51 +0000
Subject: [PATCH 3/4] :globe_with_meridians: [V2] Sync Translations + Update
README Progress Table (#4754)
### Description of Changes
This Pull Request was automatically generated to synchronize updates to
translation files and documentation for the **V2 branch**. Below are the
details of the changes made:
#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.json`) to reflect changes in the
reference file `en-GB/translation.json`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
#### **2. Update README.md**
- Generated the translation progress table in `README.md`.
- Added a summary of the current translation status for all supported
languages.
- Included up-to-date statistics on translation coverage.
#### **Why these changes are necessary**
- Keeps translation files aligned with the latest reference updates.
- Ensures the documentation reflects the current translation progress.
---
Auto-generated by [create-pull-request][1].
[1]: https://github.com/peter-evans/create-pull-request
Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
---
README.md | 74 ++++++++++++++++-----------------
scripts/ignore_translation.toml | 1 -
2 files changed, 37 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
index f8058d906..211301b99 100644
--- a/README.md
+++ b/README.md
@@ -115,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
-| Arabic (العربية) (ar_AR) |  |
-| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
-| Basque (Euskara) (eu_ES) |  |
-| Bulgarian (Български) (bg_BG) |  |
-| Catalan (Català) (ca_CA) |  |
-| Croatian (Hrvatski) (hr_HR) |  |
-| Czech (Česky) (cs_CZ) |  |
-| Danish (Dansk) (da_DK) |  |
-| Dutch (Nederlands) (nl_NL) |  |
+| Arabic (العربية) (ar_AR) |  |
+| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
+| Basque (Euskara) (eu_ES) |  |
+| Bulgarian (Български) (bg_BG) |  |
+| Catalan (Català) (ca_CA) |  |
+| Croatian (Hrvatski) (hr_HR) |  |
+| Czech (Česky) (cs_CZ) |  |
+| Danish (Dansk) (da_DK) |  |
+| Dutch (Nederlands) (nl_NL) |  |
| English (English) (en_GB) |  |
| English (US) (en_US) |  |
-| French (Français) (fr_FR) |  |
-| German (Deutsch) (de_DE) |  |
-| Greek (Ελληνικά) (el_GR) |  |
-| Hindi (हिंदी) (hi_IN) |  |
-| Hungarian (Magyar) (hu_HU) |  |
-| Indonesian (Bahasa Indonesia) (id_ID) |  |
-| Irish (Gaeilge) (ga_IE) |  |
-| Italian (Italiano) (it_IT) |  |
-| Japanese (日本語) (ja_JP) |  |
-| Korean (한국어) (ko_KR) |  |
-| Norwegian (Norsk) (no_NB) |  |
-| Persian (فارسی) (fa_IR) |  |
-| Polish (Polski) (pl_PL) |  |
-| Portuguese (Português) (pt_PT) |  |
-| Portuguese Brazilian (Português) (pt_BR) |  |
-| Romanian (Română) (ro_RO) |  |
-| Russian (Русский) (ru_RU) |  |
-| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
-| Simplified Chinese (简体中文) (zh_CN) |  |
-| Slovakian (Slovensky) (sk_SK) |  |
-| Slovenian (Slovenščina) (sl_SI) |  |
-| Spanish (Español) (es_ES) |  |
-| Swedish (Svenska) (sv_SE) |  |
-| Thai (ไทย) (th_TH) |  |
+| French (Français) (fr_FR) |  |
+| German (Deutsch) (de_DE) |  |
+| Greek (Ελληνικά) (el_GR) |  |
+| Hindi (हिंदी) (hi_IN) |  |
+| Hungarian (Magyar) (hu_HU) |  |
+| Indonesian (Bahasa Indonesia) (id_ID) |  |
+| Irish (Gaeilge) (ga_IE) |  |
+| Italian (Italiano) (it_IT) |  |
+| Japanese (日本語) (ja_JP) |  |
+| Korean (한국어) (ko_KR) |  |
+| Norwegian (Norsk) (no_NB) |  |
+| Persian (فارسی) (fa_IR) |  |
+| Polish (Polski) (pl_PL) |  |
+| Portuguese (Português) (pt_PT) |  |
+| Portuguese Brazilian (Português) (pt_BR) |  |
+| Romanian (Română) (ro_RO) |  |
+| Russian (Русский) (ru_RU) |  |
+| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
+| Simplified Chinese (简体中文) (zh_CN) |  |
+| Slovakian (Slovensky) (sk_SK) |  |
+| Slovenian (Slovenščina) (sl_SI) |  |
+| Spanish (Español) (es_ES) |  |
+| Swedish (Svenska) (sv_SE) |  |
+| Thai (ไทย) (th_TH) |  |
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
-| Traditional Chinese (繁體中文) (zh_TW) |  |
-| Turkish (Türkçe) (tr_TR) |  |
-| Ukrainian (Українська) (uk_UA) |  |
-| Vietnamese (Tiếng Việt) (vi_VN) |  |
+| Traditional Chinese (繁體中文) (zh_TW) |  |
+| Turkish (Türkçe) (tr_TR) |  |
+| Ukrainian (Українська) (uk_UA) |  |
+| Vietnamese (Tiếng Việt) (vi_VN) |  |
| Malayalam (മലയാളം) (ml_IN) |  |
## Stirling PDF Enterprise
diff --git a/scripts/ignore_translation.toml b/scripts/ignore_translation.toml
index f0d1d7d2b..83e2df925 100644
--- a/scripts/ignore_translation.toml
+++ b/scripts/ignore_translation.toml
@@ -242,7 +242,6 @@ ignore = [
'team.status',
'text',
'update.version',
- 'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1',
From 50760b5302251821913ee872b9a10db2683c14c2 Mon Sep 17 00:00:00 2001
From: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:04:09 +0000
Subject: [PATCH 4/4] quick fix to compare (#4893)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description of Changes
- Compare fix to stop the pages from getting stuck in loading state.
This code tracks which page images have actually finished loading in a
Map stored in a ref, and then uses a throwaway useState value
(setImageLoadedTick) just to force a re-render when that ref changes.
When an
fires onLoad, it marks that page as loaded in
imageLoadedRef and bumps the tick, causing React to re-render and
re-evaluate the condition for the loader overlay. Because the overlay is
only shown when imageLoadedRef.current.get(page.pageNumber) is false,
that re-render hides the blur/loader once the page’s image has loaded
and is in view, instead of the UI getting stuck in the “loading” state.
---
## 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.
---
.../core/components/tools/compare/CompareDocumentPane.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx b/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx
index 78f9a4b75..18adf6ac6 100644
--- a/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx
+++ b/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx
@@ -1,5 +1,5 @@
import { Group, Loader, Stack, Text } from '@mantine/core';
-import { useMemo, useRef, useEffect } from 'react';
+import { useMemo, useRef, useEffect, useState } from 'react';
import type { PagePreview } from '@app/types/compare';
import type { TokenBoundingBox, CompareDocumentPaneProps } from '@app/types/compare';
import { mergeConnectedRects, normalizeRotation, groupWordRects, computePageLayoutMetrics } from '@app/components/tools/compare/compare';
@@ -53,6 +53,8 @@ const CompareDocumentPane = ({
// Track which page images have finished loading to avoid flashing between states
const imageLoadedRef = useRef