mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Remove ambiguous translations and fix invalid translations (#4841)
# Description of Changes
`i18next` allows this pattern for translations, which we use quite a few
times in our current translation files:
```json
{
"a": {
"b": "hello"
},
"a.b": "world"
}
```
This makes it ambiguous when selecting `a.b` which string will be
retrieved. We have seen issues in other languages in the current release
like this:
<img width="325" height="249" alt="image"
src="https://github.com/user-attachments/assets/f24a29f0-550f-49b8-b355-c5e5eb436558"
/>
because we are expecting this:
<img width="1022" height="210" alt="image"
src="https://github.com/user-attachments/assets/b6d5cdd4-96cd-4b2b-8f1a-465da8bf70c8"
/>
but the Spanish file has:
<img width="312" height="136" alt="image"
src="https://github.com/user-attachments/assets/1e13392c-8484-47d1-b0c4-19d52b3ea5eb"
/>
and no `removeDigitalSignature` key on its own.
This PR resolves all of these ambiguities in the source by restructuring
all of the keys to uniquely target either an object or a string, not
both. It also adds a test which will fail on any keys with a `.` in
their name, therefore making it impossible to add anything ambiguous.
This commit is contained in:
+35
-35
@@ -205,7 +205,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableAlphaFunctionality', 'Enable Alpha Features')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableAlphaFunctionality.label', 'Enable Alpha Features')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.enableAlphaFunctionality.description', 'Enable experimental and alpha-stage features (may be unstable)')}
|
||||
</Text>
|
||||
@@ -221,7 +221,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableUrlToPDF', 'Enable URL to PDF')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.enableUrlToPDF.label', 'Enable URL to PDF')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.enableUrlToPDF.description', 'Allow conversion of web pages to PDF documents (internal use only)')}
|
||||
</Text>
|
||||
@@ -237,7 +237,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.disableSanitize', 'Disable HTML Sanitization')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.disableSanitize.label', 'Disable HTML Sanitization')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.disableSanitize.description', 'Disable HTML sanitization (WARNING: Security risk - can lead to XSS injections)')}
|
||||
</Text>
|
||||
@@ -262,7 +262,7 @@ export default function AdminAdvancedSection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.advanced.maxDPI', 'Maximum DPI')}</span>
|
||||
<span>{t('admin.settings.advanced.maxDPI.label', 'Maximum DPI')}</span>
|
||||
<PendingBadge show={isFieldPending('maxDPI')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -278,7 +278,7 @@ export default function AdminAdvancedSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.advanced.tessdataDir', 'Tessdata Directory')}</span>
|
||||
<span>{t('admin.settings.advanced.tessdataDir.label', 'Tessdata Directory')}</span>
|
||||
<PendingBadge show={isFieldPending('tessdataDir')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -295,7 +295,7 @@ export default function AdminAdvancedSection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.advanced.tempFileManagement', 'Temp File Management')}</Text>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.advanced.tempFileManagement.label', 'Temp File Management')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.advanced.tempFileManagement.description', 'Configure temporary file storage and cleanup behavior')}
|
||||
</Text>
|
||||
@@ -303,7 +303,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.baseTmpDir', 'Base Temp Directory')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.baseTmpDir.label', 'Base Temp Directory')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.baseTmpDir.description', 'Base directory for temporary files (leave empty for default: java.io.tmpdir/stirling-pdf)')}
|
||||
value={settings.tempFileManagement?.baseTmpDir || ''}
|
||||
onChange={(e) => setSettings({
|
||||
@@ -316,7 +316,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.libreofficeDir', 'LibreOffice Temp Directory')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.libreofficeDir.label', 'LibreOffice Temp Directory')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.libreofficeDir.description', 'Directory for LibreOffice temp files (leave empty for default: baseTmpDir/libreoffice)')}
|
||||
value={settings.tempFileManagement?.libreofficeDir || ''}
|
||||
onChange={(e) => setSettings({
|
||||
@@ -329,7 +329,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.systemTempDir', 'System Temp Directory')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.systemTempDir.label', 'System Temp Directory')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.systemTempDir.description', 'System temp directory to clean (only used if cleanupSystemTemp is enabled)')}
|
||||
value={settings.tempFileManagement?.systemTempDir || ''}
|
||||
onChange={(e) => setSettings({
|
||||
@@ -342,7 +342,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.prefix', 'Temp File Prefix')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.prefix.label', 'Temp File Prefix')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.prefix.description', 'Prefix for temp file names')}
|
||||
value={settings.tempFileManagement?.prefix || 'stirling-pdf-'}
|
||||
onChange={(e) => setSettings({
|
||||
@@ -355,7 +355,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.maxAgeHours', 'Max Age (hours)')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.maxAgeHours.label', 'Max Age (hours)')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.maxAgeHours.description', 'Maximum age in hours before temp files are cleaned up')}
|
||||
value={settings.tempFileManagement?.maxAgeHours ?? 24}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -369,7 +369,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.tempFileManagement.cleanupIntervalMinutes', 'Cleanup Interval (minutes)')}
|
||||
label={t('admin.settings.advanced.tempFileManagement.cleanupIntervalMinutes.label', 'Cleanup Interval (minutes)')}
|
||||
description={t('admin.settings.advanced.tempFileManagement.cleanupIntervalMinutes.description', 'How often to run cleanup (in minutes)')}
|
||||
value={settings.tempFileManagement?.cleanupIntervalMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -383,7 +383,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.tempFileManagement.startupCleanup', 'Startup Cleanup')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.tempFileManagement.startupCleanup.label', 'Startup Cleanup')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.tempFileManagement.startupCleanup.description', 'Clean up old temp files on application startup')}
|
||||
</Text>
|
||||
@@ -402,7 +402,7 @@ export default function AdminAdvancedSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.tempFileManagement.cleanupSystemTemp', 'Cleanup System Temp')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.advanced.tempFileManagement.cleanupSystemTemp.label', 'Cleanup System Temp')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.advanced.tempFileManagement.cleanupSystemTemp.description', 'Whether to clean broader system temp directory (use with caution)')}
|
||||
</Text>
|
||||
@@ -424,7 +424,7 @@ export default function AdminAdvancedSection() {
|
||||
{/* Process Executor Limits */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm">{t('admin.settings.advanced.processExecutor', 'Process Executor Limits')}</Text>
|
||||
<Text fw={600} size="sm">{t('admin.settings.advanced.processExecutor.label', 'Process Executor Limits')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.advanced.processExecutor.description', 'Configure session limits and timeouts for each process executor')}
|
||||
</Text>
|
||||
@@ -436,7 +436,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.libreOfficeSessionLimit ?? 1}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -450,7 +450,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.libreOfficetimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -473,7 +473,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.pdfToHtmlSessionLimit ?? 1}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -487,7 +487,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.pdfToHtmltimeoutMinutes ?? 20}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -510,7 +510,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.qpdfSessionLimit ?? 4}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -524,7 +524,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.qpdfTimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -547,7 +547,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.tesseractSessionLimit ?? 1}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -561,7 +561,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.tesseractTimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -584,7 +584,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.pythonOpenCvSessionLimit ?? 8}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -598,7 +598,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.pythonOpenCvtimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -621,7 +621,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.weasyPrintSessionLimit ?? 16}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -635,7 +635,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.weasyPrinttimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -658,7 +658,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.installAppSessionLimit ?? 1}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -672,7 +672,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.installApptimeoutMinutes ?? 60}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -695,7 +695,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.calibreSessionLimit ?? 1}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -709,7 +709,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.calibretimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -732,7 +732,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.ghostscriptSessionLimit ?? 8}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -746,7 +746,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.ghostscriptTimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -769,7 +769,7 @@ export default function AdminAdvancedSection() {
|
||||
<Accordion.Panel>
|
||||
<Stack gap="sm">
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit', 'Session Limit')}
|
||||
label={t('admin.settings.advanced.processExecutor.sessionLimit.label', 'Session Limit')}
|
||||
description={t('admin.settings.advanced.processExecutor.sessionLimit.description', 'Maximum concurrent instances')}
|
||||
value={settings.processExecutor?.sessionLimit?.ocrMyPdfSessionLimit ?? 2}
|
||||
onChange={(value) => setSettings({
|
||||
@@ -783,7 +783,7 @@ export default function AdminAdvancedSection() {
|
||||
max={100}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t('admin.settings.advanced.processExecutor.timeout', 'Timeout (minutes)')}
|
||||
label={t('admin.settings.advanced.processExecutor.timeout.label', 'Timeout (minutes)')}
|
||||
description={t('admin.settings.advanced.processExecutor.timeout.description', 'Maximum execution time')}
|
||||
value={settings.processExecutor?.timeoutMinutes?.ocrMyPdfTimeoutMinutes ?? 30}
|
||||
onChange={(value) => setSettings({
|
||||
|
||||
+1
-1
@@ -328,7 +328,7 @@ export default function AdminConnectionsSection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.ssoAutoLogin', 'SSO Auto Login')}</Text>
|
||||
<Text fw={600} size="sm">{t('admin.settings.connections.ssoAutoLogin.label', 'SSO Auto Login')}</Text>
|
||||
<Badge color="yellow" size="sm">PRO</Badge>
|
||||
</Group>
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function AdminDatabaseSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.database.enableCustom', 'Enable Custom Database')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.database.enableCustom.label', 'Enable Custom Database')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.database.enableCustom.description', 'Use your own custom database configuration instead of the default embedded database')}
|
||||
</Text>
|
||||
@@ -143,7 +143,7 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.customUrl', 'Custom Database URL')}</span>
|
||||
<span>{t('admin.settings.database.customUrl.label', 'Custom Database URL')}</span>
|
||||
<PendingBadge show={isFieldPending('customDatabaseUrl')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -158,7 +158,7 @@ export default function AdminDatabaseSection() {
|
||||
<Select
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.type', 'Database Type')}</span>
|
||||
<span>{t('admin.settings.database.type.label', 'Database Type')}</span>
|
||||
<PendingBadge show={isFieldPending('type')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -178,7 +178,7 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.hostName', 'Host Name')}</span>
|
||||
<span>{t('admin.settings.database.hostName.label', 'Host Name')}</span>
|
||||
<PendingBadge show={isFieldPending('hostName')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -193,7 +193,7 @@ export default function AdminDatabaseSection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.port', 'Port')}</span>
|
||||
<span>{t('admin.settings.database.port.label', 'Port')}</span>
|
||||
<PendingBadge show={isFieldPending('port')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -209,7 +209,7 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.name', 'Database Name')}</span>
|
||||
<span>{t('admin.settings.database.name.label', 'Database Name')}</span>
|
||||
<PendingBadge show={isFieldPending('name')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -224,7 +224,7 @@ export default function AdminDatabaseSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.username', 'Username')}</span>
|
||||
<span>{t('admin.settings.database.username.label', 'Username')}</span>
|
||||
<PendingBadge show={isFieldPending('username')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -239,7 +239,7 @@ export default function AdminDatabaseSection() {
|
||||
<PasswordInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.database.password', 'Password')}</span>
|
||||
<span>{t('admin.settings.database.password.label', 'Password')}</span>
|
||||
<PendingBadge show={isFieldPending('password')} />
|
||||
</Group>
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function AdminEndpointsSection() {
|
||||
<MultiSelect
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.endpoints.toRemove', 'Disabled Endpoints')}</span>
|
||||
<span>{t('admin.settings.endpoints.toRemove.label', 'Disabled Endpoints')}</span>
|
||||
<PendingBadge show={isFieldPending('toRemove')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export default function AdminEndpointsSection() {
|
||||
<MultiSelect
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.endpoints.groupsToRemove', 'Disabled Endpoint Groups')}</span>
|
||||
<span>{t('admin.settings.endpoints.groupsToRemove.label', 'Disabled Endpoint Groups')}</span>
|
||||
<PendingBadge show={isFieldPending('groupsToRemove')} />
|
||||
</Group>
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function AdminFeaturesSection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.features.serverCertificate', 'Server Certificate')}</Text>
|
||||
<Text fw={600} size="sm">{t('admin.settings.features.serverCertificate.label', 'Server Certificate')}</Text>
|
||||
<Badge color="blue" size="sm">PRO</Badge>
|
||||
</Group>
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function AdminFeaturesSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.features.serverCertificate.enabled', 'Enable Server Certificate')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.features.serverCertificate.enabled.label', 'Enable Server Certificate')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.features.serverCertificate.enabled.description', 'Enable server-side certificate for "Sign with Stirling-PDF" option')}
|
||||
</Text>
|
||||
@@ -137,7 +137,7 @@ export default function AdminFeaturesSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.features.serverCertificate.organizationName', 'Organization Name')}</span>
|
||||
<span>{t('admin.settings.features.serverCertificate.organizationName.label', 'Organization Name')}</span>
|
||||
<PendingBadge show={isFieldPending('serverCertificate.organizationName')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export default function AdminFeaturesSection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.features.serverCertificate.validity', 'Certificate Validity (days)')}</span>
|
||||
<span>{t('admin.settings.features.serverCertificate.validity.label', 'Certificate Validity (days)')}</span>
|
||||
<PendingBadge show={isFieldPending('serverCertificate.validity')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export default function AdminFeaturesSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.features.serverCertificate.regenerateOnStartup', 'Regenerate on Startup')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.features.serverCertificate.regenerateOnStartup.label', 'Regenerate on Startup')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.features.serverCertificate.regenerateOnStartup.description', 'Generate new certificate on each application startup')}
|
||||
</Text>
|
||||
|
||||
@@ -180,7 +180,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.appNameNavbar', 'Navbar Brand')}</span>
|
||||
<span>{t('admin.settings.general.appNameNavbar.label', 'Navbar Brand')}</span>
|
||||
<PendingBadge show={isFieldPending('ui.appNameNavbar')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -195,7 +195,7 @@ export default function AdminGeneralSection() {
|
||||
<MultiSelect
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.languages', 'Available Languages')}</span>
|
||||
<span>{t('admin.settings.general.languages.label', 'Available Languages')}</span>
|
||||
<PendingBadge show={isFieldPending('ui.languages')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -225,7 +225,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.defaultLocale', 'Default Locale')}</span>
|
||||
<span>{t('admin.settings.general.defaultLocale.label', 'Default Locale')}</span>
|
||||
<PendingBadge show={isFieldPending('system.defaultLocale')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -240,7 +240,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.fileUploadLimit', 'File Upload Limit')}</span>
|
||||
<span>{t('admin.settings.general.fileUploadLimit.label', 'File Upload Limit')}</span>
|
||||
<PendingBadge show={isFieldPending('system.fileUploadLimit')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -253,7 +253,7 @@ export default function AdminGeneralSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdate', 'Show Update Notifications')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdate.label', 'Show Update Notifications')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.showUpdate.description', 'Display notifications when a new version is available')}
|
||||
</Text>
|
||||
@@ -269,7 +269,7 @@ export default function AdminGeneralSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdateOnlyAdmin', 'Show Updates to Admins Only')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.showUpdateOnlyAdmin.label', 'Show Updates to Admins Only')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.showUpdateOnlyAdmin.description', 'Restrict update notifications to admin users only')}
|
||||
</Text>
|
||||
@@ -285,7 +285,7 @@ export default function AdminGeneralSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.customHTMLFiles', 'Custom HTML Files')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.customHTMLFiles.label', 'Custom HTML Files')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.customHTMLFiles.description', 'Allow serving custom HTML files from the customFiles directory')}
|
||||
</Text>
|
||||
@@ -305,13 +305,13 @@ export default function AdminGeneralSection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.general.customMetadata', 'Custom Metadata')}</Text>
|
||||
<Text fw={600} size="sm">{t('admin.settings.general.customMetadata.label', 'Custom Metadata')}</Text>
|
||||
<Badge color="yellow" size="sm">PRO</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.customMetadata.autoUpdate', 'Auto Update Metadata')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.general.customMetadata.autoUpdate.label', 'Auto Update Metadata')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.general.customMetadata.autoUpdate.description', 'Automatically update PDF metadata on all processed documents')}
|
||||
</Text>
|
||||
@@ -335,7 +335,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customMetadata.author', 'Default Author')}</span>
|
||||
<span>{t('admin.settings.general.customMetadata.author.label', 'Default Author')}</span>
|
||||
<PendingBadge show={isFieldPending('customMetadata.author')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -356,7 +356,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customMetadata.creator', 'Default Creator')}</span>
|
||||
<span>{t('admin.settings.general.customMetadata.creator.label', 'Default Creator')}</span>
|
||||
<PendingBadge show={isFieldPending('customMetadata.creator')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -377,7 +377,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customMetadata.producer', 'Default Producer')}</span>
|
||||
<span>{t('admin.settings.general.customMetadata.producer.label', 'Default Producer')}</span>
|
||||
<PendingBadge show={isFieldPending('customMetadata.producer')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -400,19 +400,19 @@ export default function AdminGeneralSection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.customPaths', 'Custom Paths')}</Text>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.general.customPaths.label', 'Custom Paths')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.general.customPaths.description', 'Configure custom file system paths for pipeline processing and external tools')}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Text fw={500} size="sm" mt="xs">{t('admin.settings.general.customPaths.pipeline', 'Pipeline Directories')}</Text>
|
||||
<Text fw={500} size="sm" mt="xs">{t('admin.settings.general.customPaths.pipeline.label', 'Pipeline Directories')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customPaths.pipeline.watchedFoldersDir', 'Watched Folders Directory')}</span>
|
||||
<span>{t('admin.settings.general.customPaths.pipeline.watchedFoldersDir.label', 'Watched Folders Directory')}</span>
|
||||
<PendingBadge show={isFieldPending('customPaths.pipeline.watchedFoldersDir')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customPaths.pipeline.finishedFoldersDir', 'Finished Folders Directory')}</span>
|
||||
<span>{t('admin.settings.general.customPaths.pipeline.finishedFoldersDir.label', 'Finished Folders Directory')}</span>
|
||||
<PendingBadge show={isFieldPending('customPaths.pipeline.finishedFoldersDir')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -456,13 +456,13 @@ export default function AdminGeneralSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text fw={500} size="sm" mt="md">{t('admin.settings.general.customPaths.operations', 'External Tool Paths')}</Text>
|
||||
<Text fw={500} size="sm" mt="md">{t('admin.settings.general.customPaths.operations.label', 'External Tool Paths')}</Text>
|
||||
|
||||
<div>
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customPaths.operations.weasyprint', 'WeasyPrint Executable')}</span>
|
||||
<span>{t('admin.settings.general.customPaths.operations.weasyprint.label', 'WeasyPrint Executable')}</span>
|
||||
<PendingBadge show={isFieldPending('customPaths.operations.weasyprint')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -486,7 +486,7 @@ export default function AdminGeneralSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.general.customPaths.operations.unoconvert', 'Unoconvert Executable')}</span>
|
||||
<span>{t('admin.settings.general.customPaths.operations.unoconvert.label', 'Unoconvert Executable')}</span>
|
||||
<PendingBadge show={isFieldPending('customPaths.operations.unoconvert')} />
|
||||
</Group>
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export default function AdminLegalSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.legal.termsAndConditions', 'Terms and Conditions')}</span>
|
||||
<span>{t('admin.settings.legal.termsAndConditions.label', 'Terms and Conditions')}</span>
|
||||
<PendingBadge show={isFieldPending('termsAndConditions')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export default function AdminLegalSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.legal.privacyPolicy', 'Privacy Policy')}</span>
|
||||
<span>{t('admin.settings.legal.privacyPolicy.label', 'Privacy Policy')}</span>
|
||||
<PendingBadge show={isFieldPending('privacyPolicy')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export default function AdminLegalSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.legal.accessibilityStatement', 'Accessibility Statement')}</span>
|
||||
<span>{t('admin.settings.legal.accessibilityStatement.label', 'Accessibility Statement')}</span>
|
||||
<PendingBadge show={isFieldPending('accessibilityStatement')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -132,7 +132,7 @@ export default function AdminLegalSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.legal.cookiePolicy', 'Cookie Policy')}</span>
|
||||
<span>{t('admin.settings.legal.cookiePolicy.label', 'Cookie Policy')}</span>
|
||||
<PendingBadge show={isFieldPending('cookiePolicy')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -147,7 +147,7 @@ export default function AdminLegalSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.legal.impressum', 'Impressum')}</span>
|
||||
<span>{t('admin.settings.legal.impressum.label', 'Impressum')}</span>
|
||||
<PendingBadge show={isFieldPending('impressum')} />
|
||||
</Group>
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function AdminMailSection() {
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.mail.enabled', 'Enable Mail')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.mail.enabled.label', 'Enable Mail')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.mail.enabled.description', 'Enable email notifications and SMTP functionality')}
|
||||
</Text>
|
||||
@@ -138,7 +138,7 @@ export default function AdminMailSection() {
|
||||
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.mail.enableInvites', 'Enable Email Invites')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.mail.enableInvites.label', 'Enable Email Invites')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.mail.enableInvites.description', 'Allow admins to invite users via email with auto-generated passwords')}
|
||||
</Text>
|
||||
@@ -157,7 +157,7 @@ export default function AdminMailSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.host', 'SMTP Host')}</span>
|
||||
<span>{t('admin.settings.mail.host.label', 'SMTP Host')}</span>
|
||||
<PendingBadge show={isFieldPending('host')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export default function AdminMailSection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.port', 'SMTP Port')}</span>
|
||||
<span>{t('admin.settings.mail.port.label', 'SMTP Port')}</span>
|
||||
<PendingBadge show={isFieldPending('port')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -188,7 +188,7 @@ export default function AdminMailSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.username', 'SMTP Username')}</span>
|
||||
<span>{t('admin.settings.mail.username.label', 'SMTP Username')}</span>
|
||||
<PendingBadge show={isFieldPending('username')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export default function AdminMailSection() {
|
||||
<PasswordInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.password', 'SMTP Password')}</span>
|
||||
<span>{t('admin.settings.mail.password.label', 'SMTP Password')}</span>
|
||||
<PendingBadge show={isFieldPending('password')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -216,7 +216,7 @@ export default function AdminMailSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.from', 'From Address')}</span>
|
||||
<span>{t('admin.settings.mail.from.label', 'From Address')}</span>
|
||||
<PendingBadge show={isFieldPending('from')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export default function AdminMailSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.mail.frontendUrl', 'Frontend URL')}</span>
|
||||
<span>{t('admin.settings.mail.frontendUrl.label', 'Frontend URL')}</span>
|
||||
<PendingBadge show={isFieldPending('frontendUrl')} />
|
||||
</Group>
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function AdminPremiumSection() {
|
||||
<TextInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.premium.key', 'License Key')}</span>
|
||||
<span>{t('admin.settings.premium.key.label', 'License Key')}</span>
|
||||
<PendingBadge show={isFieldPending('key')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export default function AdminPremiumSection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.enabled', 'Enable Premium Features')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.premium.enabled.label', 'Enable Premium Features')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.premium.enabled.description', 'Enable license key checks for pro/enterprise features')}
|
||||
</Text>
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function AdminPrivacySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.enableAnalytics', 'Enable Analytics')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.enableAnalytics.label', 'Enable Analytics')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.enableAnalytics.description', 'Collect anonymous usage analytics to help improve the application')}
|
||||
</Text>
|
||||
@@ -132,7 +132,7 @@ export default function AdminPrivacySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.metricsEnabled', 'Enable Metrics')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.metricsEnabled.label', 'Enable Metrics')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.metricsEnabled.description', 'Enable collection of performance and usage metrics')}
|
||||
</Text>
|
||||
@@ -155,7 +155,7 @@ export default function AdminPrivacySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.googleVisibility', 'Google Visibility')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.privacy.googleVisibility.label', 'Google Visibility')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.privacy.googleVisibility.description', 'Allow search engines to index this application')}
|
||||
</Text>
|
||||
|
||||
+25
-25
@@ -197,7 +197,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.enableLogin', 'Enable Login')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.enableLogin.label', 'Enable Login')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.enableLogin.description', 'Require users to log in before accessing the application')}
|
||||
</Text>
|
||||
@@ -213,7 +213,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div>
|
||||
<Select
|
||||
label={t('admin.settings.security.loginMethod', 'Login Method')}
|
||||
label={t('admin.settings.security.loginMethod.label', 'Login Method')}
|
||||
description={t('admin.settings.security.loginMethod.description', 'The authentication method to use for user login')}
|
||||
value={settings.loginMethod || 'all'}
|
||||
onChange={(value) => setSettings({ ...settings, loginMethod: value || 'all' })}
|
||||
@@ -236,7 +236,7 @@ export default function AdminSecuritySection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.loginAttemptCount', 'Login Attempt Limit')}</span>
|
||||
<span>{t('admin.settings.security.loginAttemptCount.label', 'Login Attempt Limit')}</span>
|
||||
<PendingBadge show={isFieldPending('loginAttemptCount')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -252,7 +252,7 @@ export default function AdminSecuritySection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.loginResetTimeMinutes', 'Login Reset Time (minutes)')}</span>
|
||||
<span>{t('admin.settings.security.loginResetTimeMinutes.label', 'Login Reset Time (minutes)')}</span>
|
||||
<PendingBadge show={isFieldPending('loginResetTimeMinutes')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -266,7 +266,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.csrfDisabled', 'Disable CSRF Protection')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.csrfDisabled.label', 'Disable CSRF Protection')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.csrfDisabled.description', 'Disable Cross-Site Request Forgery protection (not recommended)')}
|
||||
</Text>
|
||||
@@ -297,11 +297,11 @@ export default function AdminSecuritySection() {
|
||||
{/* JWT Settings */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.jwt', 'JWT Configuration')}</Text>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.jwt.label', 'JWT Configuration')}</Text>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.persistence', 'Enable Key Persistence')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.persistence.label', 'Enable Key Persistence')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.persistence.description', 'Store JWT keys persistently (required for multi-instance deployments)')}
|
||||
</Text>
|
||||
@@ -317,7 +317,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyRotation', 'Enable Key Rotation')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyRotation.label', 'Enable Key Rotation')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.enableKeyRotation.description', 'Automatically rotate JWT signing keys for improved security')}
|
||||
</Text>
|
||||
@@ -333,7 +333,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyCleanup', 'Enable Key Cleanup')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.enableKeyCleanup.label', 'Enable Key Cleanup')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.enableKeyCleanup.description', 'Automatically remove old JWT keys after retention period')}
|
||||
</Text>
|
||||
@@ -351,7 +351,7 @@ export default function AdminSecuritySection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.jwt.keyRetentionDays', 'Key Retention Days')}</span>
|
||||
<span>{t('admin.settings.security.jwt.keyRetentionDays.label', 'Key Retention Days')}</span>
|
||||
<PendingBadge show={isFieldPending('jwt.keyRetentionDays')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -365,7 +365,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.secureCookie', 'Secure Cookie')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.jwt.secureCookie.label', 'Secure Cookie')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.jwt.secureCookie.description', 'Require HTTPS for JWT cookies (recommended for production)')}
|
||||
</Text>
|
||||
@@ -385,13 +385,13 @@ export default function AdminSecuritySection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text fw={600} size="sm">{t('admin.settings.security.audit', 'Audit Logging')}</Text>
|
||||
<Text fw={600} size="sm">{t('admin.settings.security.audit.label', 'Audit Logging')}</Text>
|
||||
<Badge color="grape" size="sm">ENTERPRISE</Badge>
|
||||
</Group>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.audit.enabled', 'Enable Audit Logging')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.audit.enabled.label', 'Enable Audit Logging')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.audit.enabled.description', 'Track user actions and system events for compliance and security monitoring')}
|
||||
</Text>
|
||||
@@ -409,7 +409,7 @@ export default function AdminSecuritySection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.audit.level', 'Audit Level')}</span>
|
||||
<span>{t('admin.settings.security.audit.level.label', 'Audit Level')}</span>
|
||||
<PendingBadge show={isFieldPending('audit.level')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -425,7 +425,7 @@ export default function AdminSecuritySection() {
|
||||
<NumberInput
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.audit.retentionDays', 'Audit Retention (days)')}</span>
|
||||
<span>{t('admin.settings.security.audit.retentionDays.label', 'Audit Retention (days)')}</span>
|
||||
<PendingBadge show={isFieldPending('audit.retentionDays')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -443,7 +443,7 @@ export default function AdminSecuritySection() {
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.htmlUrlSecurity', 'HTML URL Security')}</Text>
|
||||
<Text fw={600} size="sm" mb="xs">{t('admin.settings.security.htmlUrlSecurity.label', 'HTML URL Security')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('admin.settings.security.htmlUrlSecurity.description', 'Configure URL access restrictions for HTML processing to prevent SSRF attacks')}
|
||||
</Text>
|
||||
@@ -451,7 +451,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.enabled', 'Enable URL Security')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.enabled.label', 'Enable URL Security')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.htmlUrlSecurity.enabled.description', 'Enable URL security restrictions for HTML to PDF conversions')}
|
||||
</Text>
|
||||
@@ -475,7 +475,7 @@ export default function AdminSecuritySection() {
|
||||
<Select
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.level', 'Security Level')}</span>
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.level.label', 'Security Level')}</span>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.level')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -507,7 +507,7 @@ export default function AdminSecuritySection() {
|
||||
<Textarea
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.allowedDomains', 'Allowed Domains (Whitelist)')}</span>
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.allowedDomains.label', 'Allowed Domains (Whitelist)')}</span>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.allowedDomains')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -534,7 +534,7 @@ export default function AdminSecuritySection() {
|
||||
<Textarea
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.blockedDomains', 'Blocked Domains (Blacklist)')}</span>
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.blockedDomains.label', 'Blocked Domains (Blacklist)')}</span>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.blockedDomains')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -561,7 +561,7 @@ export default function AdminSecuritySection() {
|
||||
<Textarea
|
||||
label={
|
||||
<Group gap="xs">
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.internalTlds', 'Internal TLDs')}</span>
|
||||
<span>{t('admin.settings.security.htmlUrlSecurity.internalTlds.label', 'Internal TLDs')}</span>
|
||||
<PendingBadge show={isFieldPending('html.urlSecurity.internalTlds')} />
|
||||
</Group>
|
||||
}
|
||||
@@ -588,7 +588,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockPrivateNetworks', 'Block Private Networks')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockPrivateNetworks.label', 'Block Private Networks')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.htmlUrlSecurity.blockPrivateNetworks.description', 'Block RFC 1918 private networks (10.x.x.x, 192.168.x.x, 172.16-31.x.x)')}
|
||||
</Text>
|
||||
@@ -610,7 +610,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockLocalhost', 'Block Localhost')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockLocalhost.label', 'Block Localhost')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.htmlUrlSecurity.blockLocalhost.description', 'Block localhost and loopback addresses (127.x.x.x, ::1)')}
|
||||
</Text>
|
||||
@@ -632,7 +632,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockLinkLocal', 'Block Link-Local Addresses')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockLinkLocal.label', 'Block Link-Local Addresses')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.htmlUrlSecurity.blockLinkLocal.description', 'Block link-local addresses (169.254.x.x, fe80::/10)')}
|
||||
</Text>
|
||||
@@ -654,7 +654,7 @@ export default function AdminSecuritySection() {
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockCloudMetadata', 'Block Cloud Metadata Endpoints')}</Text>
|
||||
<Text fw={500} size="sm">{t('admin.settings.security.htmlUrlSecurity.blockCloudMetadata.label', 'Block Cloud Metadata Endpoints')}</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t('admin.settings.security.htmlUrlSecurity.blockCloudMetadata.description', 'Block cloud provider metadata endpoints (169.254.169.254)')}
|
||||
</Text>
|
||||
|
||||
@@ -20,7 +20,7 @@ const FlattenSettings = ({ parameters, onParameterChange, disabled = false }: Fl
|
||||
disabled={disabled}
|
||||
label={
|
||||
<div>
|
||||
<Text size="sm">{t('flatten.options.flattenOnlyForms', 'Flatten only forms')}</Text>
|
||||
<Text size="sm">{t('flatten.options.flattenOnlyForms.label', 'Flatten only forms')}</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t('flatten.options.flattenOnlyForms.desc', 'Only flatten form fields, leaving other interactive elements intact')}
|
||||
</Text>
|
||||
|
||||
@@ -93,8 +93,8 @@ describe('MergeSettings', () => {
|
||||
);
|
||||
|
||||
// Verify that translation keys are being called
|
||||
expect(mockT).toHaveBeenCalledWith('merge.removeDigitalSignature', 'Remove digital signature in the merged file?');
|
||||
expect(mockT).toHaveBeenCalledWith('merge.generateTableOfContents', 'Generate table of contents in the merged file?');
|
||||
expect(mockT).toHaveBeenCalledWith('merge.removeDigitalSignature.label', 'Remove digital signature in the merged file?');
|
||||
expect(mockT).toHaveBeenCalledWith('merge.generateTableOfContents.label', 'Generate table of contents in the merged file?');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -19,14 +19,14 @@ const MergeSettings: React.FC<MergeSettingsProps> = ({
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Checkbox
|
||||
label={t('merge.removeDigitalSignature', 'Remove digital signature in the merged file?')}
|
||||
label={t('merge.removeDigitalSignature.label', 'Remove digital signature in the merged file?')}
|
||||
checked={parameters.removeDigitalSignature}
|
||||
onChange={(event) => onParameterChange('removeDigitalSignature', event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
label={t('merge.generateTableOfContents', 'Generate table of contents in the merged file?')}
|
||||
label={t('merge.generateTableOfContents.label', 'Generate table of contents in the merged file?')}
|
||||
checked={parameters.generateTableOfContents}
|
||||
onChange={(event) => onParameterChange('generateTableOfContents', event.currentTarget.checked)}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -161,8 +161,8 @@ describe('SanitizeSettings', () => {
|
||||
|
||||
// Verify that translation keys are being called (just check that it was called, not specific order)
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.title', expect.any(String));
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeJavaScript', expect.any(String));
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeEmbeddedFiles', expect.any(String));
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeJavaScript.label', expect.any(String));
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.removeEmbeddedFiles.label', expect.any(String));
|
||||
expect(mockT).toHaveBeenCalledWith('sanitize.options.note', expect.any(String));
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ const SanitizeSettings = ({ parameters, onParameterChange, disabled = false }: S
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = (Object.keys(defaultParameters) as Array<keyof SanitizeParameters>).map((key) => ({
|
||||
key: key,
|
||||
label: t(`sanitize.options.${key}`, key),
|
||||
key,
|
||||
label: t(`sanitize.options.${key}.label`, key),
|
||||
description: t(`sanitize.options.${key}.desc`, `${key} from the PDF`),
|
||||
default: defaultParameters[key],
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const LOCALES_DIR = path.join(__dirname, '../../../public/locales');
|
||||
|
||||
const getLocaleDirectories = () => {
|
||||
if (!fs.existsSync(LOCALES_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return fs.readdirSync(LOCALES_DIR, { withFileTypes: true })
|
||||
.filter(dirent => dirent.isDirectory())
|
||||
.map(dirent => dirent.name);
|
||||
};
|
||||
|
||||
const findDottedKeys = (node: unknown, segments: string[] = []): string[] => {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const issues: string[] = [];
|
||||
for (const [key, value] of Object.entries(node as Record<string, unknown>)) {
|
||||
if (key.includes('.')) {
|
||||
issues.push([...segments, key].join('.'));
|
||||
}
|
||||
issues.push(...findDottedKeys(value, [...segments, key]));
|
||||
}
|
||||
return issues;
|
||||
};
|
||||
|
||||
const localeDirectories = getLocaleDirectories();
|
||||
|
||||
describe('Translation key structure', () => {
|
||||
test('should locate locales directory', () => {
|
||||
expect(fs.existsSync(LOCALES_DIR)).toBe(true);
|
||||
});
|
||||
|
||||
test('should have at least one locale directory', () => {
|
||||
expect(localeDirectories.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test.each(localeDirectories)('should not contain dotted keys in %s/translation.json', (localeDir) => {
|
||||
const translationFile = path.join(LOCALES_DIR, localeDir, 'translation.json');
|
||||
expect(fs.existsSync(translationFile)).toBe(true);
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(translationFile, 'utf8'));
|
||||
const dottedKeys = findDottedKeys(data);
|
||||
expect(dottedKeys, `Dotted keys found in ${localeDir}: ${dottedKeys.join(', ')}`).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -668,7 +668,7 @@ export default function PeopleSection() {
|
||||
<Stack gap="md" align="center">
|
||||
<LocalIcon icon="person-add" width="3rem" height="3rem" style={{ color: 'var(--mantine-color-gray-6)' }} />
|
||||
<Text size="xl" fw={600} ta="center">
|
||||
{t('workspace.people.inviteMembers', 'Invite Members')}
|
||||
{t('workspace.people.inviteMembers.label', 'Invite Members')}
|
||||
</Text>
|
||||
{inviteMode === 'email' && (
|
||||
<Text size="sm" c="dimmed" ta="center" px="md">
|
||||
|
||||
Reference in New Issue
Block a user