diff --git a/build.gradle b/build.gradle index fd706cb6c..0f30b3d67 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ springBoot { allprojects { group = 'stirling.software' - version = '2.5.0' + version = '2.5.1' configurations.configureEach { exclude group: 'commons-logging', module: 'commons-logging' diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 7c5b3d957..410e25f3b 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Stirling-PDF", - "version": "2.5.0", + "version": "2.5.1", "identifier": "stirling.pdf.dev", "build": { "frontendDist": "../dist", diff --git a/frontend/src/core/components/shared/LanguageSelector.tsx b/frontend/src/core/components/shared/LanguageSelector.tsx index 1eabfcf1f..66aa17c8a 100644 --- a/frontend/src/core/components/shared/LanguageSelector.tsx +++ b/frontend/src/core/components/shared/LanguageSelector.tsx @@ -52,12 +52,15 @@ const LanguageItem: React.FC = ({ }) => { const { t } = useTranslation(); + const labelText = option.label; + const comingSoonText = t('comingSoon', 'Coming soon'); + const label = disabled ? ( - -

{option.label}

+ +

{labelText}

) : ( -

{option.label}

+

{labelText}

); return ( @@ -157,12 +160,27 @@ const LanguageSelector: React.FC = ({ compact = false, tooltip }) => { - const { i18n } = useTranslation(); + const { i18n, ready } = useTranslation(); const [opened, setOpened] = useState(false); const [animationTriggered, setAnimationTriggered] = useState(false); const [pendingLanguage, setPendingLanguage] = useState(null); const [rippleEffect, setRippleEffect] = useState(null); + // Trigger animation when dropdown opens + useEffect(() => { + if (opened) { + setAnimationTriggered(false); + // Small delay to ensure DOM is ready + setTimeout(() => setAnimationTriggered(true), 20); + } + }, [opened]); + + // Don't render until i18n is ready to prevent race condition + // during SAML auth where components render before i18n initializes + if (!ready || !i18n.language) { + return null; + } + // Get the filtered list of supported languages from i18n // This respects server config (ui.languages) applied by AppConfigLoader const allowedLanguages = (i18n.options.supportedLngs as string[] || []) @@ -176,12 +194,6 @@ const LanguageSelector: React.FC = ({ label: name, })); - // Hide the language selector if there's only one language option - // (no point showing a selector when there's nothing to select) - if (languageOptions.length <= 1) { - return null; - } - // Calculate dropdown width and grid columns based on number of languages // 2-4: 300px/2 cols, 5-9: 400px/3 cols, 10+: 600px/4 cols const dropdownWidth = languageOptions.length <= 4 ? 300 @@ -225,16 +237,14 @@ const LanguageSelector: React.FC = ({ }; const currentLanguage = supportedLanguages[i18n.language as keyof typeof supportedLanguages] || - supportedLanguages['en-GB']; + supportedLanguages['en-GB'] || + 'English'; // Fallback if supportedLanguages lookup fails - // Trigger animation when dropdown opens - useEffect(() => { - if (opened) { - setAnimationTriggered(false); - // Small delay to ensure DOM is ready - setTimeout(() => setAnimationTriggered(true), 20); - } - }, [opened]); + // Hide the language selector if there's only one language option + // (no point showing a selector when there's nothing to select) + if (languageOptions.length <= 1) { + return null; + } return ( <> diff --git a/frontend/src/core/components/shared/ToolChain.tsx b/frontend/src/core/components/shared/ToolChain.tsx index d3c25f653..c67d89427 100644 --- a/frontend/src/core/components/shared/ToolChain.tsx +++ b/frontend/src/core/components/shared/ToolChain.tsx @@ -24,9 +24,8 @@ const ToolChain: React.FC = ({ size = 'xs', color = 'var(--mantine-color-blue-7)' }) => { - if (!toolChain || toolChain.length === 0) return null; - const { t } = useTranslation(); + if (!toolChain || toolChain.length === 0) return null; const toolIds = toolChain.map(tool => tool.toolId); diff --git a/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx b/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx index 18adf6ac6..c541c60f2 100644 --- a/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx +++ b/frontend/src/core/components/tools/compare/CompareDocumentPane.tsx @@ -74,6 +74,11 @@ const CompareDocumentPane = ({ } }, [zoom]); + const renderedPageNumbers = useMemo( + () => new Set(pages.map((p) => p.pageNumber)), + [pages] + ); + return (
@@ -88,7 +93,7 @@ const CompareDocumentPane = ({ placeholder={dropdownPlaceholder ?? null} className={pane === 'comparison' ? 'compare-changes-select--comparison' : undefined} onNavigate={onNavigateChange} - renderedPageNumbers={useMemo(() => new Set(pages.map(p => p.pageNumber)), [pages])} + renderedPageNumbers={renderedPageNumbers} /> )} diff --git a/frontend/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.tsx b/frontend/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.tsx index d6c80cf62..3cbc3979c 100644 --- a/frontend/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.tsx +++ b/frontend/src/core/components/tools/editTableOfContents/EditTableOfContentsWorkbenchView.tsx @@ -43,6 +43,16 @@ interface EditTableOfContentsWorkbenchViewProps { const EditTableOfContentsWorkbenchView = ({ data }: EditTableOfContentsWorkbenchViewProps) => { const { t } = useTranslation(); const terminology = useFileActionTerminology(); + const files = data?.files ?? []; + const thumbnails = data?.thumbnails ?? []; + const previewFiles = useMemo( + () => + files.map((file, index) => ({ + file, + thumbnail: thumbnails[index], + })), + [files, thumbnails] + ); if (!data) { return ( @@ -63,8 +73,6 @@ const EditTableOfContentsWorkbenchView = ({ data }: EditTableOfContentsWorkbench bookmarks, selectedFileName, disabled, - files, - thumbnails, downloadUrl, downloadFilename, errorMessage, @@ -78,15 +86,6 @@ const EditTableOfContentsWorkbenchView = ({ data }: EditTableOfContentsWorkbench onFileClick, } = data; - const previewFiles = useMemo( - () => - files?.map((file, index) => ({ - file, - thumbnail: thumbnails[index], - })) ?? [], - [files, thumbnails] - ); - const showResults = Boolean( previewFiles.length > 0 || downloadUrl || errorMessage ); diff --git a/frontend/src/core/components/tools/shared/FilesToolStep.tsx b/frontend/src/core/components/tools/shared/FilesToolStep.tsx index b1e8933f5..1d71196ac 100644 --- a/frontend/src/core/components/tools/shared/FilesToolStep.tsx +++ b/frontend/src/core/components/tools/shared/FilesToolStep.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; import FileStatusIndicator from '@app/components/tools/shared/FileStatusIndicator'; import { StirlingFile } from '@app/types/fileContext'; +import i18n from '@app/i18n'; export interface FilesToolStepProps { selectedFiles: StirlingFile[]; @@ -14,9 +14,7 @@ export function createFilesToolStep( createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement, props: FilesToolStepProps ): React.ReactElement { - const { t } = useTranslation(); - - return createStep(t("files.title", "Files"), { + return createStep(i18n.t("files.title", "Files"), { isVisible: true, isCollapsed: props.isCollapsed, onCollapsedClick: props.onCollapsedClick diff --git a/frontend/src/core/components/tools/shared/ReviewToolStep.tsx b/frontend/src/core/components/tools/shared/ReviewToolStep.tsx index 7b324d5dc..c67d933f1 100644 --- a/frontend/src/core/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/src/core/components/tools/shared/ReviewToolStep.tsx @@ -12,6 +12,7 @@ import { useFileActionIcons } from "@app/hooks/useFileActionIcons"; import { saveOperationResults } from "@app/services/operationResultsSaveService"; import { useFileActions, useFileState } from "@app/contexts/FileContext"; import { FileId } from "@app/types/fileContext"; +import i18n from "@app/i18n"; export interface ReviewToolStepProps { isVisible: boolean; @@ -151,10 +152,8 @@ export function createReviewToolStep( ) => React.ReactElement, props: ReviewToolStepProps ): React.ReactElement { - const { t } = useTranslation(); - return createStep( - t("review", "Review"), + i18n.t("review", "Review"), { isVisible: props.isVisible, isCollapsed: props.isCollapsed, diff --git a/frontend/src/core/components/tools/shared/ToolStep.tsx b/frontend/src/core/components/tools/shared/ToolStep.tsx index 89c39aad8..8d2d95513 100644 --- a/frontend/src/core/components/tools/shared/ToolStep.tsx +++ b/frontend/src/core/components/tools/shared/ToolStep.tsx @@ -80,8 +80,6 @@ const ToolStep = ({ alwaysShowTooltip = false, tooltip }: ToolStepProps) => { - if (!isVisible) return null; - const parent = useContext(ToolStepContext); // Auto-detect if we should show numbers based on sibling count or force option @@ -91,6 +89,8 @@ const ToolStep = ({ return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect }, [showNumber, parent]); + if (!isVisible) return null; + const stepNumber = _stepNumber; return ( diff --git a/frontend/src/core/testing/serverExperienceSimulations.ts b/frontend/src/core/testing/serverExperienceSimulations.ts index 9a922911b..42cd297bc 100644 --- a/frontend/src/core/testing/serverExperienceSimulations.ts +++ b/frontend/src/core/testing/serverExperienceSimulations.ts @@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.5.0', + appVersion: '2.5.1', serverCertificateEnabled: false, enableAlphaFunctionality: false, serverPort: 8080, diff --git a/frontend/src/proprietary/testing/serverExperienceSimulations.ts b/frontend/src/proprietary/testing/serverExperienceSimulations.ts index e866976d5..3356759ba 100644 --- a/frontend/src/proprietary/testing/serverExperienceSimulations.ts +++ b/frontend/src/proprietary/testing/serverExperienceSimulations.ts @@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = { const BASE_NO_LOGIN_CONFIG: AppConfig = { enableAnalytics: true, - appVersion: '2.5.0', + appVersion: '2.5.1', serverCertificateEnabled: false, enableAlphaFunctionality: false, enableDesktopInstallSlide: true, diff --git a/testing/compose/docker-compose-keycloak-saml.yml b/testing/compose/docker-compose-keycloak-saml.yml index d4a909877..c5192122c 100644 --- a/testing/compose/docker-compose-keycloak-saml.yml +++ b/testing/compose/docker-compose-keycloak-saml.yml @@ -74,7 +74,7 @@ services: DOCKER_ENABLE_SECURITY: "true" SECURITY_ENABLELOGIN: "true" SECURITY_LOGINMETHOD: "${SECURITY_LOGINMETHOD:-all}" - SYSTEM_DEFAULTLOCALE: en-US + SYSTEM_DEFAULTLOCALE: "${SYSTEM_DEFAULTLOCALE:-en-US}" SYSTEM_BACKENDURL: "http://localhost:8080" # Enterprise License (required for SAML) diff --git a/testing/compose/start-saml-test.sh b/testing/compose/start-saml-test.sh index 6a77a03d3..8037ab838 100755 --- a/testing/compose/start-saml-test.sh +++ b/testing/compose/start-saml-test.sh @@ -13,24 +13,48 @@ echo -e "${BLUE}╚════════════════════ echo "" AUTO_LOGIN=false +DEFAULT_LANGUAGE="en-US" COMPOSE_UP_ARGS=(-d --build) -for arg in "$@"; do - case "$arg" in +while [[ $# -gt 0 ]]; do + case "$1" in --auto) AUTO_LOGIN=true + shift ;; --nobuild) COMPOSE_UP_ARGS=(-d) + shift + ;; + --language) + if [[ -z "${2:-}" ]]; then + echo -e "${RED}Missing value for --language${NC}" + exit 1 + fi + DEFAULT_LANGUAGE="$2" + shift 2 + ;; + --language=*) + DEFAULT_LANGUAGE="${1#*=}" + shift + ;; + -l) + if [[ -z "${2:-}" ]]; then + echo -e "${RED}Missing value for -l${NC}" + exit 1 + fi + DEFAULT_LANGUAGE="$2" + shift 2 ;; -h|--help) - echo "Usage: $0 [--auto] [--nobuild]" + echo "Usage: $0 [--auto] [--nobuild] [--language ]" echo "" echo " --auto Enable SSO auto-login and force SAML-only login method" echo " --nobuild Skip building images (use existing images)" + echo " --language Set system default locale (e.g. de-DE, sv-SE)" exit 0 ;; *) - echo -e "${RED}Unknown option: $arg${NC}" + echo -e "${RED}Unknown option: $1${NC}" exit 1 ;; esac @@ -65,6 +89,10 @@ if [ "$AUTO_LOGIN" = true ]; then echo "" fi +export SYSTEM_DEFAULTLOCALE="$DEFAULT_LANGUAGE" +echo -e "${GREEN}✓ Default locale set to: ${SYSTEM_DEFAULTLOCALE}${NC}" +echo "" + echo -e "${YELLOW}▶ Starting Keycloak (SAML) containers...${NC}" docker-compose -f docker-compose-keycloak-saml.yml up "${COMPOSE_UP_ARGS[@]}" keycloak-saml-db keycloak-saml