From e61e9a68306b11b29291ef72561a120cac7d16ce Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 30 Sep 2025 21:23:46 -0400 Subject: [PATCH 001/100] Fix:Expose general collection prefs for picklist scoping and attachment defaults --- .../Preferences/CollectionDefinitions.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index ce8612a47fe..a250f3361e1 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -11,6 +11,36 @@ import type { GenericPreferences } from './types'; import { definePref } from './types'; export const collectionPreferenceDefinitions = { + general: { + title: preferencesText.general(), + subCategories: { + behavior: { + title: preferencesText.behavior(), + items: { + sp7_scope_table_picklists: definePref({ + title: localized( + "Limit 'Entire Table' picklists to values used by this collection" + ), + requiresReload: false, + visible: true, + defaultValue: true, + renderer: f.never, + container: 'label', + type: 'java.lang.Boolean', + }), + 'attachment.is_public_default': definePref({ + title: localized('Make new attachments public by default'), + requiresReload: false, + visible: true, + defaultValue: false, + renderer: f.never, + container: 'label', + type: 'java.lang.Boolean', + }), + }, + }, + }, + }, statistics: { title: statsText.statistics(), subCategories: { @@ -28,7 +58,7 @@ export const collectionPreferenceDefinitions = { showPreparationsTotal: definePref({ title: localized('Defines if preparation stats include total'), requiresReload: false, - visible: false, + visible: true, defaultValue: true, renderer: f.never, container: 'label', @@ -37,7 +67,7 @@ export const collectionPreferenceDefinitions = { refreshRate: definePref({ title: localized('_Defines the rate of auto refresh in hours'), requiresReload: false, - visible: false, + visible: true, defaultValue: 24, renderer: f.never, container: 'label', From 87c5e3252b2bb4d32e89746ceee71a533c264416 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 2 Oct 2025 17:04:15 -0400 Subject: [PATCH 002/100] implement Collection Preferences UI with dedicated definitions --- .../AppResources/TabDefinitions.tsx | 3 +- .../Preferences/CollectionDefinitions.tsx | 246 ++++++++++++++---- 2 files changed, 193 insertions(+), 56 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx index f4109fcffe8..53c59369fac 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx @@ -26,6 +26,7 @@ import { formattersSpec } from '../Formatters/spec'; import { FormEditor } from '../FormEditor'; import { viewSetsSpec } from '../FormEditor/spec'; import { UserPreferencesEditor } from '../Preferences/Editor'; +import { CollectionPreferencesEditor } from '../Preferences/CollectionPreferencesPage'; import { useDarkMode } from '../Preferences/Hooks'; import type { BaseSpec } from '../Syncer'; import type { SimpleXmlNode } from '../Syncer/xmlToJson'; @@ -154,7 +155,7 @@ export const visualAppResourceEditors = f.store< json: AppResourceTextEditor, }, collectionPreferences: { - // FEATURE: add visual editor + visual: CollectionPreferencesEditor, json: AppResourceTextEditor, }, leafletLayers: undefined, diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index a250f3361e1..448b2173046 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -1,3 +1,7 @@ +import React from 'react'; + +import { attachmentsText } from '../../localization/attachments'; +import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; import { specifyNetworkText } from '../../localization/specifyNetwork'; @@ -5,42 +9,151 @@ import { statsText } from '../../localization/stats'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure, localized } from '../../utils/types'; -import type { QueryView } from '../QueryBuilder/Header'; +import { Link } from '../Atoms/Link'; import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; +const documentationLink = (url: string) => ( + {headerText.documentation()} +); + +const picklistDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562' +); + +const attachmentDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640' +); + +const treeDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4' +); + +const statisticsDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715' +); + +const specifyNetworkDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793' +); + +const catalogDocs = documentationLink( + 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859' +); + export const collectionPreferenceDefinitions = { general: { title: preferencesText.general(), subCategories: { - behavior: { - title: preferencesText.behavior(), + pickLists: { + title: localized('Pick lists'), items: { sp7_scope_table_picklists: definePref({ - title: localized( - "Limit 'Entire Table' picklists to values used by this collection" + title: localized('Scope “Entire Table” picklists'), + description: ( + <> + {localized( + 'Restrict “Entire Table” picklists to values used by records in this collection.' + )}{' '} + {picklistDocs} + ), requiresReload: false, visible: true, - defaultValue: true, - renderer: f.never, - container: 'label', + defaultValue: false, type: 'java.lang.Boolean', }), + }, + }, + attachments: { + title: attachmentsText.attachments(), + items: { 'attachment.is_public_default': definePref({ - title: localized('Make new attachments public by default'), + title: localized('New attachments are public'), + description: ( + <> + {localized( + 'Set the default visibility for attachments created within this collection.' + )}{' '} + {attachmentDocs} + + ), requiresReload: false, visible: true, defaultValue: false, - renderer: f.never, - container: 'label', type: 'java.lang.Boolean', }), }, }, }, }, + treeManagement: { + title: localized('Tree management'), + subCategories: { + synonymized: { + title: localized('Synonymized nodes'), + description: treeDocs, + items: { + 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': + definePref({ + title: localized( + 'Allow children under synonymized Geologic Time Period nodes' + ), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + 'sp7.allow_adding_child_to_synonymized_parent.Taxon': definePref({ + title: localized('Allow children under synonymized Taxon nodes'), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + 'sp7.allow_adding_child_to_synonymized_parent.Geography': + definePref({ + title: localized( + 'Allow children under synonymized Geography nodes' + ), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + 'sp7.allow_adding_child_to_synonymized_parent.LithoStrat': + definePref({ + title: localized( + 'Allow children under synonymized Lithostratigraphy nodes' + ), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + 'sp7.allow_adding_child_to_synonymized_parent.Storage': + definePref({ + title: localized('Allow children under synonymized Storage nodes'), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + 'sp7.allow_adding_child_to_synonymized_parent.TectonicUnit': + definePref({ + title: localized( + 'Allow children under synonymized Tectonic Unit nodes' + ), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), + }, + }, + }, + }, statistics: { title: statsText.statistics(), subCategories: { @@ -56,71 +169,90 @@ export const collectionPreferenceDefinitions = { container: 'label', }), showPreparationsTotal: definePref({ - title: localized('Defines if preparation stats include total'), + title: localized('Show preparation totals'), + description: ( + <> + {localized( + 'Include an overall total across preparation types on the statistics page.' + )}{' '} + {statisticsDocs} + + ), requiresReload: false, visible: true, defaultValue: true, - renderer: f.never, - container: 'label', type: 'java.lang.Boolean', }), refreshRate: definePref({ - title: localized('_Defines the rate of auto refresh in hours'), + title: localized('Auto-refresh rate (hours)'), + description: ( + <> + {localized( + 'Specify how frequently shared statistics refresh their data.' + )}{' '} + {statisticsDocs} + + ), requiresReload: false, visible: true, defaultValue: 24, - renderer: f.never, - container: 'label', - type: 'java.lang.Float', + type: 'java.lang.Integer', }), }, }, - specifyNetwork: { - title: specifyNetworkText.specifyNetwork(), + }, + }, + specifyNetwork: { + title: specifyNetworkText.specifyNetwork(), + subCategories: { + gbif: { + title: localized('GBIF'), items: { publishingOrganization: definePref({ - title: localized('_Stores GBIF\'s "publishingOrgKey"'), + title: localized('Publishing organization key'), + description: ( + <> + {localized( + 'GBIF publishingOrgKey for this collection when contributing to the Specify Network.' + )}{' '} + {specifyNetworkDocs} + + ), requiresReload: false, - visible: false, + visible: true, defaultValue: undefined, - renderer: f.never, - container: 'label', + type: 'java.lang.String', }), collectionKey: definePref({ - title: localized('_Stores GBIF\'s "dataSetKey"'), + title: localized('Collection key'), + description: ( + <> + {localized( + 'GBIF dataSetKey used for this collection in the Specify Network.' + )}{' '} + {specifyNetworkDocs} + + ), requiresReload: false, - visible: false, + visible: true, defaultValue: undefined, - renderer: f.never, - container: 'label', - }), - }, - }, - }, - }, - queryBuilder: { - title: queryText.queryBuilder(), - subCategories: { - appearance: { - title: preferencesText.appearance(), - items: { - display: definePref({ - title: preferencesText.displayBasicView(), - requiresReload: false, - visible: false, - defaultValue: { - basicView: [], - detailedView: [], - }, - renderer: f.never, - container: 'div', + type: 'java.lang.String', }), }, }, }, }, + catalogNumberInheritance: { title: queryText.catalogNumberInheritance(), + description: ( + <> + {localized( + 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.' + )}{' '} + {catalogDocs} + + ), subCategories: { behavior: { title: preferencesText.behavior(), @@ -128,10 +260,8 @@ export const collectionPreferenceDefinitions = { inheritance: definePref({ title: preferencesText.inheritanceCatNumberPref(), requiresReload: false, - visible: false, + visible: true, defaultValue: false, - renderer: f.never, - container: 'label', type: 'java.lang.Boolean', }), }, @@ -140,6 +270,14 @@ export const collectionPreferenceDefinitions = { }, catalogNumberParentInheritance: { title: queryText.catalogNumberParentCOInheritance(), + description: ( + <> + {localized( + 'Control whether component records inherit catalog numbers from their parent Collection Object.' + )}{' '} + {catalogDocs} + + ), subCategories: { behavior: { title: preferencesText.behavior(), @@ -147,10 +285,8 @@ export const collectionPreferenceDefinitions = { inheritance: definePref({ title: preferencesText.inheritanceCatNumberParentCOPref(), requiresReload: false, - visible: false, + visible: true, defaultValue: false, - renderer: f.never, - container: 'label', type: 'java.lang.Boolean', }), }, From f8c579a9747d38d98aeb3d3523b67542ae2e5c89 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 2 Oct 2025 17:07:34 -0400 Subject: [PATCH 003/100] adding missing files for the Collection preferences --- .../Preferences/CollectionPreferencesPage.tsx | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx new file mode 100644 index 00000000000..4e4060dfb13 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx @@ -0,0 +1,417 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { usePromise } from '../../hooks/useAsyncState'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { useLiveState } from '../../hooks/useLiveState'; +import { commonText } from '../../localization/common'; +import { preferencesText } from '../../localization/preferences'; +import { Container, H2 } from '../Atoms'; +import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; +import { Form } from '../Atoms/Form'; +import { Link } from '../Atoms/Link'; +import { Submit } from '../Atoms/Submit'; +import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; +import { ProtectedTool } from '../Permissions/PermissionDenied'; +import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; +import { fetchContext as fetchRemotePrefs, getCollectionPref, getPref } from '../InitialContext/remotePrefs'; +import { schema } from '../DataModel/schema'; +import { collectionPreferences } from './collectionPreferences'; +import { collectionPreferenceDefinitions } from './CollectionDefinitions'; +import { DefaultPreferenceItemRender } from './Renderers'; +import type { GenericPreferences } from './types'; +import { BasePreferences } from './BasePreferences'; +import { useTopChild } from './useTopChild'; + +const preferencesPromise = collectionPreferences.fetch().then(() => true); + +type CollectionPreferencesInstance = BasePreferences; +type ItemEntry = ReturnType[number][1]['subCategories'][number][1]['items'][number]; + +function useCollectionPrefDefinitions() { + const visibilityContext = React.useMemo( + () => ({ isDarkMode: false, isRedirecting: false }), + [] + ); + return React.useMemo(() => + Object.entries(collectionPreferenceDefinitions as GenericPreferences) + .map(([category, { subCategories, ...categoryData }]) => [ + category, + { + ...categoryData, + subCategories: Object.entries(subCategories) + .map(([subcategory, { items, ...subcategoryData }]) => [ + subcategory, + { + ...subcategoryData, + items: Object.entries(items).filter(([, item]) => + typeof item.visible === 'function' + ? item.visible(visibilityContext) + : item.visible !== false + ), + }, + ] as const) + .filter(([, { items }]) => items.length > 0), + }, + ] as const) + .filter(([, { subCategories }]) => subCategories.length > 0), + [visibilityContext]); +} + +function CollectionPreferencesAside({ + definitions, + activeCategory, + setActiveCategory, + references, +}: { + readonly definitions: ReturnType; + readonly activeCategory: number | undefined; + readonly setActiveCategory: (index: number | undefined) => void; + readonly references: ReturnType['references']; +}): JSX.Element { + return ( + + ); +} + +function CollectionPreferencesContent({ + definitions, + preferences, + forwardRefs, +}: { + readonly definitions: ReturnType; + readonly preferences: CollectionPreferencesInstance; + readonly forwardRefs?: (index: number, element: HTMLElement | null) => void; +}): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); + + return ( +
+ {definitions.map(([category, { title, description, subCategories }], index) => ( + + +

{typeof title === 'function' ? title() : title}

+ {description !== undefined && ( +

+ {typeof description === 'function' ? description() : description} +

+ )} + {subCategories.map(([subcategory, { title: subTitle, description: subDescription, items }]) => ( +
+
+

+ {typeof subTitle === 'function' ? subTitle() : subTitle} +

+
+ + items.forEach(([name]) => { + const definition = preferences.definition( + category as never, + subcategory as never, + name as never + ); + preferences.set( + category as never, + subcategory as never, + name as never, + definition.defaultValue as never + ); + }) + } + > + {commonText.reset()} + +
+
+ {subDescription !== undefined && ( +

+ {typeof subDescription === 'function' + ? subDescription() + : subDescription} +

+ )} + {items.map((itemEntry) => ( + + ))} +
+ ))} +
+
+ ))} +
+ ); +} + +function CollectionPreferenceItem({ + category, + subcategory, + itemEntry, + preferences, + isReadOnly, +}: { + readonly category: string; + readonly subcategory: string; + readonly itemEntry: ItemEntry; + readonly preferences: CollectionPreferencesInstance; + readonly isReadOnly: boolean; +}): JSX.Element { + const [name, item] = itemEntry; + const [value, setValue] = preferences.use( + category as never, + subcategory as never, + name as never + ); + + const handleChange = React.useCallback( + (newValue: unknown) => { + if (isReadOnly) return; + setValue(newValue as never); + }, + [isReadOnly, setValue] + ); + + const Renderer = 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; + const props = { + className: `flex items-start gap-2 md:flex-row flex-col ${ + isReadOnly ? '!cursor-not-allowed' : '' + }`, + key: name, + } as const; + + const content = ( + <> +
+

+ {typeof item.title === 'function' ? item.title() : item.title} +

+ {item.description !== undefined && ( +

+ {typeof item.description === 'function' ? item.description() : item.description} +

+ )} +
+
+ + + +
+ + ); + + return 'container' in item && item.container === 'div' ? ( +
{content}
+ ) : ( + + ); +} + +function CollectionPreferences(): JSX.Element { + const [changesMade, markChangesMade, clearChanges] = useBooleanState(); + const loading = React.useContext(LoadingContext); + const navigate = useNavigate(); + const { + visibleChild, + setVisibleChild, + forwardRefs, + references, + scrollContainerRef, + } = useTopChild(); + const definitions = useCollectionPrefDefinitions(); + + React.useEffect(() => { + let cancelled = false; + + const migrateFromRemotePrefs = async () => { + await fetchRemotePrefs; + if (cancelled) return; + + const collectionId = schema.domainLevelIds.collection; + const migrationTargets: ReadonlyArray<{ + readonly category: string; + readonly subcategory: string; + readonly name: string; + readonly value: () => boolean; + }> = [ + { + category: 'general', + subcategory: 'pickLists', + name: 'sp7_scope_table_picklists', + value: () => getCollectionPref('sp7_scope_table_picklists', collectionId), + }, + { + category: 'general', + subcategory: 'attachments', + name: 'attachment.is_public_default', + value: () => getPref('attachment.is_public_default'), + }, + ...['GeologicTimePeriod', 'Taxon', 'Geography', 'LithoStrat', 'Storage', 'TectonicUnit'].map( + (treeName) => ({ + category: 'treeManagement', + subcategory: 'synonymized', + name: `sp7.allow_adding_child_to_synonymized_parent.${treeName}`, + value: () => getPref(`sp7.allow_adding_child_to_synonymized_parent.${treeName}`), + }) + ), + ]; + + migrationTargets.forEach(({ category, subcategory, name, value }) => { + const current = (collectionPreferences.getRaw() as Record)[category]?.[subcategory]?.[name]; + if (current !== undefined) return; + const definition = collectionPreferences.definition( + category as never, + subcategory as never, + name as never + ); + const remoteValue = value(); + if (remoteValue === definition.defaultValue) return; + collectionPreferences.set( + category as never, + subcategory as never, + name as never, + remoteValue as never + ); + }); + }; + + void migrateFromRemotePrefs(); + + return () => { + cancelled = true; + }; + }, []); + + React.useEffect(() => + collectionPreferences.events.on('update', () => { + markChangesMade(); + }), [markChangesMade]); + + return ( + + +

{preferencesText.collectionPreferences()}

+
+ loading( + collectionPreferences + .awaitSynced() + .then(() => { + clearChanges(); + navigate('/specify/'); + }) + ) + } + > +
+ + + +
+
+ {changesMade ? ( + {commonText.save()} + ) : ( + + {commonText.close()} + + )} +
+
+
+
+ ); +} + +export function CollectionPreferencesWrapper(): JSX.Element | null { + const [hasFetched] = usePromise(preferencesPromise, true); + return hasFetched ? : null; +} + +export function CollectionPreferencesEditor({ + data, + onChange, +}: AppResourceTabProps): JSX.Element { + const [preferencesInstance] = useLiveState( + React.useCallback(() => { + const prefs = new BasePreferences({ + definitions: collectionPreferenceDefinitions, + values: { + resourceName: 'CollectionPreferences', + fetchUrl: '/context/collection_resource/', + }, + defaultValues: undefined, + developmentGlobal: '_editingCollectionPreferences', + syncChanges: false, + }); + prefs.setRaw(JSON.parse(data === null || data.length === 0 ? '{}' : data)); + prefs.events.on('update', () => onChange(JSON.stringify(prefs.getRaw()))); + return prefs; + }, [onChange]) + ); + + const definitions = useCollectionPrefDefinitions(); + const Context = preferencesInstance.Context; + + return ( + + + + + + ); +} From ff7788d56cd2ea7184a3605bdf398e27e65a17fc Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 2 Oct 2025 21:25:58 -0400 Subject: [PATCH 004/100] resolve typecheck errors in Collection Preferences definitions --- .../Preferences/CollectionDefinitions.tsx | 35 ++++--------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 448b2173046..24909fd525f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { attachmentsText } from '../../localization/attachments'; -import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; import { specifyNetworkText } from '../../localization/specifyNetwork'; @@ -9,38 +8,16 @@ import { statsText } from '../../localization/stats'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure, localized } from '../../utils/types'; -import { Link } from '../Atoms/Link'; import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; -const documentationLink = (url: string) => ( - {headerText.documentation()} -); - -const picklistDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562' -); - -const attachmentDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640' -); - -const treeDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4' -); - -const statisticsDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715' -); - -const specifyNetworkDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793' -); - -const catalogDocs = documentationLink( - 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859' -); +const picklistDocs = 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562'; +const attachmentDocs = 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640'; +const treeDocs = 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4'; +const statisticsDocs = 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715'; +const specifyNetworkDocs = 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793'; +const catalogDocs = 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859'; export const collectionPreferenceDefinitions = { general: { From 44b1da0e9f843ef25f39fd0cadc006055d60cb0a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 3 Oct 2025 20:45:04 -0400 Subject: [PATCH 005/100] add Collection Preferences UI and editor with doc links --- .../AppResources/TabDefinitions.tsx | 2 +- .../Preferences/CollectionDefinitions.tsx | 93 +-- .../Preferences/CollectionPreferencesPage.tsx | 560 ++++++++---------- .../lib/components/Preferences/Editor.tsx | 36 ++ 4 files changed, 307 insertions(+), 384 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx index 53c59369fac..0dd13ded6a6 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/TabDefinitions.tsx @@ -26,7 +26,7 @@ import { formattersSpec } from '../Formatters/spec'; import { FormEditor } from '../FormEditor'; import { viewSetsSpec } from '../FormEditor/spec'; import { UserPreferencesEditor } from '../Preferences/Editor'; -import { CollectionPreferencesEditor } from '../Preferences/CollectionPreferencesPage'; +import { CollectionPreferencesEditor } from '../Preferences/Editor'; import { useDarkMode } from '../Preferences/Hooks'; import type { BaseSpec } from '../Syncer'; import type { SimpleXmlNode } from '../Syncer/xmlToJson'; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 24909fd525f..9bf3f5e7b73 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -1,4 +1,7 @@ -import React from 'react'; +/** + * Definitions for Collection preferences + */ +import type { LocalizedString } from 'typesafe-i18n'; import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; @@ -12,29 +15,17 @@ import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; -const picklistDocs = 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562'; -const attachmentDocs = 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640'; -const treeDocs = 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4'; -const statisticsDocs = 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715'; -const specifyNetworkDocs = 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793'; -const catalogDocs = 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859'; - export const collectionPreferenceDefinitions = { general: { title: preferencesText.general(), subCategories: { pickLists: { - title: localized('Pick lists'), + title: preferencesText.filterPickLists?.() ?? (localized('Pick lists') as LocalizedString), items: { sp7_scope_table_picklists: definePref({ title: localized('Scope “Entire Table” picklists'), - description: ( - <> - {localized( - 'Restrict “Entire Table” picklists to values used by records in this collection.' - )}{' '} - {picklistDocs} - + description: localized( + 'Restrict “Entire Table” picklists to values used by records in this collection.' ), requiresReload: false, visible: true, @@ -48,13 +39,8 @@ export const collectionPreferenceDefinitions = { items: { 'attachment.is_public_default': definePref({ title: localized('New attachments are public'), - description: ( - <> - {localized( - 'Set the default visibility for attachments created within this collection.' - )}{' '} - {attachmentDocs} - + description: localized( + 'Set the default visibility for attachments created within this collection.' ), requiresReload: false, visible: true, @@ -65,12 +51,15 @@ export const collectionPreferenceDefinitions = { }, }, }, + treeManagement: { title: localized('Tree management'), subCategories: { synonymized: { title: localized('Synonymized nodes'), - description: treeDocs, + description: localized( + 'Allow creating children under synonymized nodes in specific trees.' + ), items: { 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': definePref({ @@ -147,13 +136,8 @@ export const collectionPreferenceDefinitions = { }), showPreparationsTotal: definePref({ title: localized('Show preparation totals'), - description: ( - <> - {localized( - 'Include an overall total across preparation types on the statistics page.' - )}{' '} - {statisticsDocs} - + description: localized( + 'Include an overall total across preparation types on the statistics page.' ), requiresReload: false, visible: true, @@ -162,13 +146,8 @@ export const collectionPreferenceDefinitions = { }), refreshRate: definePref({ title: localized('Auto-refresh rate (hours)'), - description: ( - <> - {localized( - 'Specify how frequently shared statistics refresh their data.' - )}{' '} - {statisticsDocs} - + description: localized( + 'Specify how frequently shared statistics refresh their data.' ), requiresReload: false, visible: true, @@ -179,7 +158,7 @@ export const collectionPreferenceDefinitions = { }, }, }, - specifyNetwork: { + specifyNetwork: { title: specifyNetworkText.specifyNetwork(), subCategories: { gbif: { @@ -187,14 +166,6 @@ export const collectionPreferenceDefinitions = { items: { publishingOrganization: definePref({ title: localized('Publishing organization key'), - description: ( - <> - {localized( - 'GBIF publishingOrgKey for this collection when contributing to the Specify Network.' - )}{' '} - {specifyNetworkDocs} - - ), requiresReload: false, visible: true, defaultValue: undefined, @@ -202,14 +173,6 @@ export const collectionPreferenceDefinitions = { }), collectionKey: definePref({ title: localized('Collection key'), - description: ( - <> - {localized( - 'GBIF dataSetKey used for this collection in the Specify Network.' - )}{' '} - {specifyNetworkDocs} - - ), requiresReload: false, visible: true, defaultValue: undefined, @@ -219,16 +182,11 @@ export const collectionPreferenceDefinitions = { }, }, }, - + catalogNumberInheritance: { title: queryText.catalogNumberInheritance(), - description: ( - <> - {localized( - 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.' - )}{' '} - {catalogDocs} - + description: localized( + 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.' ), subCategories: { behavior: { @@ -247,13 +205,8 @@ export const collectionPreferenceDefinitions = { }, catalogNumberParentInheritance: { title: queryText.catalogNumberParentCOInheritance(), - description: ( - <> - {localized( - 'Control whether component records inherit catalog numbers from their parent Collection Object.' - )}{' '} - {catalogDocs} - + description: localized( + 'Control whether component records inherit catalog numbers from their parent Collection Object.' ), subCategories: { behavior: { diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx index 4e4060dfb13..8c939535309 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx @@ -1,176 +1,177 @@ +/** + * Edit collection preferences + */ + import React from 'react'; import { useNavigate } from 'react-router-dom'; +import type { LocalizedString } from 'typesafe-i18n'; import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { useLiveState } from '../../hooks/useLiveState'; import { commonText } from '../../localization/common'; import { preferencesText } from '../../localization/preferences'; -import { Container, H2 } from '../Atoms'; +import { headerText } from '../../localization/header'; +import { StringToJsx } from '../../localization/utils'; +import { f } from '../../utils/functools'; +import { Container, Key } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { Form } from '../Atoms/Form'; import { Link } from '../Atoms/Link'; -import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { ProtectedTool } from '../Permissions/PermissionDenied'; -import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; -import { fetchContext as fetchRemotePrefs, getCollectionPref, getPref } from '../InitialContext/remotePrefs'; -import { schema } from '../DataModel/schema'; +import { hasPermission } from '../Permissions/helpers'; + import { collectionPreferences } from './collectionPreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { DefaultPreferenceItemRender } from './Renderers'; -import type { GenericPreferences } from './types'; -import { BasePreferences } from './BasePreferences'; -import { useTopChild } from './useTopChild'; +import type { GenericPreferences, PreferenceItem } from './types'; -const preferencesPromise = collectionPreferences.fetch().then(() => true); +const preferencesPromise = Promise.all([collectionPreferences.fetch()]).then( + f.true +); -type CollectionPreferencesInstance = BasePreferences; -type ItemEntry = ReturnType[number][1]['subCategories'][number][1]['items'][number]; +const DOCS = { + picklists: + 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562', + attachments: + 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640', + trees: + 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', + stats: 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + specifyNetwork: + 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', + catalogNumbers: + 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859', +} as const; -function useCollectionPrefDefinitions() { +export function useCollectionPrefDefinitions() { const visibilityContext = React.useMemo( () => ({ isDarkMode: false, isRedirecting: false }), [] ); - return React.useMemo(() => - Object.entries(collectionPreferenceDefinitions as GenericPreferences) - .map(([category, { subCategories, ...categoryData }]) => [ - category, - { - ...categoryData, - subCategories: Object.entries(subCategories) - .map(([subcategory, { items, ...subcategoryData }]) => [ - subcategory, + return React.useMemo( + () => + Object.entries(collectionPreferenceDefinitions as GenericPreferences) + .map( + ([category, { subCategories, ...categoryData }]) => + [ + category, { - ...subcategoryData, - items: Object.entries(items).filter(([, item]) => - typeof item.visible === 'function' - ? item.visible(visibilityContext) - : item.visible !== false - ), + ...categoryData, + subCategories: Object.entries(subCategories) + .map( + ([subCategory, { items, ...subCategoryData }]) => + [ + subCategory, + { + ...subCategoryData, + items: Object.entries(items).filter( + ([_name, { visible }]) => + typeof visible === 'function' + ? visible(visibilityContext) + : visible !== false + ), + }, + ] as const + ) + .filter(([_name, { items }]) => items.length > 0), }, - ] as const) - .filter(([, { items }]) => items.length > 0), - }, - ] as const) - .filter(([, { subCategories }]) => subCategories.length > 0), - [visibilityContext]); -} - -function CollectionPreferencesAside({ - definitions, - activeCategory, - setActiveCategory, - references, -}: { - readonly definitions: ReturnType; - readonly activeCategory: number | undefined; - readonly setActiveCategory: (index: number | undefined) => void; - readonly references: ReturnType['references']; -}): JSX.Element { - return ( - + ] as const + ) + .filter(([_name, { subCategories }]) => subCategories.length > 0), + [visibilityContext] ); } -function CollectionPreferencesContent({ - definitions, - preferences, - forwardRefs, -}: { - readonly definitions: ReturnType; - readonly preferences: CollectionPreferencesInstance; - readonly forwardRefs?: (index: number, element: HTMLElement | null) => void; -}): JSX.Element { - const isReadOnly = React.useContext(ReadOnlyContext); +type ItemEntry = ReturnType< + typeof useCollectionPrefDefinitions +>[number][1]['subCategories'][number][1]['items'][number]; + +export function CollectionPreferencesContent(): JSX.Element { + const definitions = useCollectionPrefDefinitions(); return (
- {definitions.map(([category, { title, description, subCategories }], index) => ( - - -

{typeof title === 'function' ? title() : title}

- {description !== undefined && ( -

- {typeof description === 'function' ? description() : description} -

- )} - {subCategories.map(([subcategory, { title: subTitle, description: subDescription, items }]) => ( -
-
-

- {typeof subTitle === 'function' ? subTitle() : subTitle} -

-
- - items.forEach(([name]) => { - const definition = preferences.definition( - category as never, - subcategory as never, - name as never - ); - preferences.set( - category as never, - subcategory as never, - name as never, - definition.defaultValue as never - ); - }) - } - > - {commonText.reset()} - -
-
- {subDescription !== undefined && ( -

- {typeof subDescription === 'function' - ? subDescription() - : subDescription} -

- )} - {items.map((itemEntry) => ( - - ))} -
- ))} -
-
- ))} + {definitions.map( + ( + [category, { title, description = undefined, subCategories }]) => ( + + +

+ {typeof title === 'function' ? title() : title} +

+ + {description !== undefined && ( +

+ {typeof description === 'function' + ? description() + : description} +

+ )} + + {subCategories.map( + ([subcategory, { title, description: subDesc, items }]) => ( +
+
+

+ {typeof title === 'function' ? title() : title} +

+ +
+ + items.forEach(([name]) => { + const def = collectionPreferences.definition( + category as never, + subcategory as never, + name as never + ); + collectionPreferences.set( + category as never, + subcategory as never, + name as never, + (def as { defaultValue: unknown }) + .defaultValue as never + ); + }) + } + > + {commonText.reset()} + +
+
+ + {subDesc !== undefined && ( +

+ {typeof subDesc === 'function' ? subDesc() : subDesc} +

+ )} + + {items.map((entry) => ( + + ))} +
+ ) + )} +
+
+ ) + )}
); } @@ -179,59 +180,97 @@ function CollectionPreferenceItem({ category, subcategory, itemEntry, - preferences, - isReadOnly, }: { readonly category: string; readonly subcategory: string; readonly itemEntry: ItemEntry; - readonly preferences: CollectionPreferencesInstance; - readonly isReadOnly: boolean; }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); const [name, item] = itemEntry; - const [value, setValue] = preferences.use( + + const canEdit = + !isReadOnly && + (item.visible !== 'protected' || + hasPermission('/preferences/user', 'edit_protected')); + + const [value, setValue] = collectionPreferences.use( category as never, subcategory as never, name as never ); - const handleChange = React.useCallback( - (newValue: unknown) => { - if (isReadOnly) return; - setValue(newValue as never); - }, - [isReadOnly, setValue] - ); + const Renderer = + 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; + + // Minimal doc link mapping + const docHref: string | undefined = (() => { + if (name === 'sp7_scope_table_picklists') return DOCS.picklists; + if (name === 'attachment.is_public_default') return DOCS.attachments; + if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) + return DOCS.trees; + if (name === 'showPreparationsTotal' || name === 'refreshRate') + return DOCS.stats; + if (name === 'publishingOrganization' || name === 'collectionKey') + return DOCS.specifyNetwork; + if (category.startsWith('catalogNumber')) return DOCS.catalogNumbers; + return undefined; + })(); - const Renderer = 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; - const props = { - className: `flex items-start gap-2 md:flex-row flex-col ${ - isReadOnly ? '!cursor-not-allowed' : '' - }`, - key: name, + const wrapperProps = { + className: ` + flex items-start gap-2 md:flex-row flex-col + ${canEdit ? '' : '!cursor-not-allowed'} + `, + title: canEdit ? undefined : preferencesText.adminsOnlyPreference(), } as const; const content = ( <>

- {typeof item.title === 'function' ? item.title() : item.title} +

- {item.description !== undefined && ( + + {(item.description !== undefined || docHref) && (

- {typeof item.description === 'function' ? item.description() : item.description} + {item.description !== undefined && ( + + )} + {docHref && ( + <> + {item.description ? ' ' : null} + + {headerText.documentation + ? headerText.documentation() + : ('Documentation' as LocalizedString)} + + + )}

)}
+
- + } item={name} subcategory={subcategory} value={value} - onChange={handleChange} + onChange={setValue} />
@@ -239,9 +278,30 @@ function CollectionPreferenceItem({ ); return 'container' in item && item.container === 'div' ? ( -
{content}
+
{content}
) : ( - + + ); +} + +function FormatString({ + text, +}: { + readonly text: JSX.Element | LocalizedString; +}): JSX.Element { + return typeof text === 'object' ? ( + text + ) : text.includes('') ? ( + + {key}, + }} + string={text} + /> + + ) : ( + <>{text} ); } @@ -249,169 +309,43 @@ function CollectionPreferences(): JSX.Element { const [changesMade, markChangesMade, clearChanges] = useBooleanState(); const loading = React.useContext(LoadingContext); const navigate = useNavigate(); - const { - visibleChild, - setVisibleChild, - forwardRefs, - references, - scrollContainerRef, - } = useTopChild(); - const definitions = useCollectionPrefDefinitions(); - React.useEffect(() => { - let cancelled = false; - - const migrateFromRemotePrefs = async () => { - await fetchRemotePrefs; - if (cancelled) return; - - const collectionId = schema.domainLevelIds.collection; - const migrationTargets: ReadonlyArray<{ - readonly category: string; - readonly subcategory: string; - readonly name: string; - readonly value: () => boolean; - }> = [ - { - category: 'general', - subcategory: 'pickLists', - name: 'sp7_scope_table_picklists', - value: () => getCollectionPref('sp7_scope_table_picklists', collectionId), - }, - { - category: 'general', - subcategory: 'attachments', - name: 'attachment.is_public_default', - value: () => getPref('attachment.is_public_default'), - }, - ...['GeologicTimePeriod', 'Taxon', 'Geography', 'LithoStrat', 'Storage', 'TectonicUnit'].map( - (treeName) => ({ - category: 'treeManagement', - subcategory: 'synonymized', - name: `sp7.allow_adding_child_to_synonymized_parent.${treeName}`, - value: () => getPref(`sp7.allow_adding_child_to_synonymized_parent.${treeName}`), - }) - ), - ]; - - migrationTargets.forEach(({ category, subcategory, name, value }) => { - const current = (collectionPreferences.getRaw() as Record)[category]?.[subcategory]?.[name]; - if (current !== undefined) return; - const definition = collectionPreferences.definition( - category as never, - subcategory as never, - name as never - ); - const remoteValue = value(); - if (remoteValue === definition.defaultValue) return; - collectionPreferences.set( - category as never, - subcategory as never, - name as never, - remoteValue as never - ); - }); - }; - - void migrateFromRemotePrefs(); - - return () => { - cancelled = true; - }; - }, []); - - React.useEffect(() => - collectionPreferences.events.on('update', () => { - markChangesMade(); - }), [markChangesMade]); + React.useEffect( + () => collectionPreferences.events.on('update', () => markChangesMade()), + [markChangesMade] + ); + return ( - -

{preferencesText.collectionPreferences()}

+ + <> +
loading( - collectionPreferences - .awaitSynced() - .then(() => { - clearChanges(); - navigate('/specify/'); - }) + collectionPreferences.awaitSynced().then(() => { + clearChanges(); + navigate('/specify/'); + }) ) } > -
- - - -
-
- {changesMade ? ( - {commonText.save()} - ) : ( - - {commonText.close()} - - )} +
+ + +
+ + - + ); } export function CollectionPreferencesWrapper(): JSX.Element | null { const [hasFetched] = usePromise(preferencesPromise, true); - return hasFetched ? : null; -} - -export function CollectionPreferencesEditor({ - data, - onChange, -}: AppResourceTabProps): JSX.Element { - const [preferencesInstance] = useLiveState( - React.useCallback(() => { - const prefs = new BasePreferences({ - definitions: collectionPreferenceDefinitions, - values: { - resourceName: 'CollectionPreferences', - fetchUrl: '/context/collection_resource/', - }, - defaultValues: undefined, - developmentGlobal: '_editingCollectionPreferences', - syncChanges: false, - }); - prefs.setRaw(JSON.parse(data === null || data.length === 0 ? '{}' : data)); - prefs.events.on('update', () => onChange(JSON.stringify(prefs.getRaw()))); - return prefs; - }, [onChange]) - ); - - const definitions = useCollectionPrefDefinitions(); - const Context = preferencesInstance.Context; - - return ( - - - - - - ); + return hasFetched === true ? : null; } diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index bac46be2988..45e3cbed8fb 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -6,6 +6,9 @@ import { PreferencesContent } from '../Preferences'; import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; +import { CollectionPreferencesContent } from './CollectionPreferencesPage'; +import { collectionPreferenceDefinitions } from './CollectionDefinitions'; +import { collectionPreferences } from './collectionPreferences'; export function UserPreferencesEditor({ data, @@ -40,3 +43,36 @@ export function UserPreferencesEditor({ ); } + +export function CollectionPreferencesEditor({ + data, + onChange, +}: AppResourceTabProps): JSX.Element { + const [preferencesInstance] = useLiveState( + React.useCallback(() => { + const tmpCollectionPrefs = new BasePreferences({ + definitions: collectionPreferenceDefinitions, + values: { + resourceName: 'CollectionPreferences', + fetchUrl: '/context/collection_resource/', + }, + defaultValues: undefined, + developmentGlobal: '_editingCollectionPreferences', + syncChanges: false, + }); + tmpCollectionPrefs.setRaw(JSON.parse(!data || data.length === 0 ? '{}' : data)); + tmpCollectionPrefs.events.on('update', () => + onChange(JSON.stringify(tmpCollectionPrefs.getRaw())) + ); + + return tmpCollectionPrefs; + }, [data, onChange]) + ); + const Context = collectionPreferences.Context; + + return ( + + + + ); +} \ No newline at end of file From e70f79688643e9e4171776d70f24d4e63b76bbae Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 3 Oct 2025 21:48:48 -0400 Subject: [PATCH 006/100] implify CollectionPreferences page by removing internal save --- .../Preferences/CollectionPreferencesPage.tsx | 41 ++----------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx index 8c939535309..951bda72546 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx @@ -3,11 +3,9 @@ */ import React from 'react'; -import { useNavigate } from 'react-router-dom'; import type { LocalizedString } from 'typesafe-i18n'; import { usePromise } from '../../hooks/useAsyncState'; -import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { preferencesText } from '../../localization/preferences'; import { headerText } from '../../localization/header'; @@ -16,9 +14,8 @@ import { f } from '../../utils/functools'; import { Container, Key } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; -import { Form } from '../Atoms/Form'; import { Link } from '../Atoms/Link'; -import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; +import { ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { ProtectedTool } from '../Permissions/PermissionDenied'; import { hasPermission } from '../Permissions/helpers'; @@ -306,41 +303,11 @@ function FormatString({ } function CollectionPreferences(): JSX.Element { - const [changesMade, markChangesMade, clearChanges] = useBooleanState(); - const loading = React.useContext(LoadingContext); - const navigate = useNavigate(); - - React.useEffect( - () => collectionPreferences.events.on('update', () => markChangesMade()), - [markChangesMade] - ); - - return ( - - <> - -
- loading( - collectionPreferences.awaitSynced().then(() => { - clearChanges(); - navigate('/specify/'); - }) - ) - } - > -
- - - -
- - -
- +
+ +
); } From ce59d26ed4185e199deca3fe3f7dfec92ec96168 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 3 Oct 2025 22:35:51 -0400 Subject: [PATCH 007/100] Corrected specifyNetwork keys to publishingOrg and datasetKey --- .../Preferences/CollectionDefinitions.tsx | 54 +++++++++++++++---- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 9bf3f5e7b73..9655e329e19 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -20,7 +20,9 @@ export const collectionPreferenceDefinitions = { title: preferencesText.general(), subCategories: { pickLists: { - title: preferencesText.filterPickLists?.() ?? (localized('Pick lists') as LocalizedString), + title: + preferencesText.filterPickLists?.() ?? + (localized('Pick lists') as LocalizedString), items: { sp7_scope_table_picklists: definePref({ title: localized('Scope “Entire Table” picklists'), @@ -71,13 +73,14 @@ export const collectionPreferenceDefinitions = { defaultValue: false, type: 'java.lang.Boolean', }), - 'sp7.allow_adding_child_to_synonymized_parent.Taxon': definePref({ - title: localized('Allow children under synonymized Taxon nodes'), - requiresReload: false, - visible: true, - defaultValue: false, - type: 'java.lang.Boolean', - }), + 'sp7.allow_adding_child_to_synonymized_parent.Taxon': + definePref({ + title: localized('Allow children under synonymized Taxon nodes'), + requiresReload: false, + visible: true, + defaultValue: false, + type: 'java.lang.Boolean', + }), 'sp7.allow_adding_child_to_synonymized_parent.Geography': definePref({ title: localized( @@ -120,6 +123,32 @@ export const collectionPreferenceDefinitions = { }, }, }, + queryBuilder: { + title: queryText.queryBuilder(), + subCategories: { + appearance: { + title: preferencesText.appearance(), + items: { + display: definePref<{ + readonly basicView: RA; + // detailedView shape isn’t used here; keep it permissive + readonly detailedView: unknown; + }>({ + title: preferencesText.displayBasicView(), + requiresReload: false, + visible: false, + defaultValue: { + basicView: [], + detailedView: {}, + }, + renderer: f.never, + container: 'label', + }), + }, + }, + }, + }, + statistics: { title: statsText.statistics(), subCategories: { @@ -158,20 +187,22 @@ export const collectionPreferenceDefinitions = { }, }, }, + specifyNetwork: { title: specifyNetworkText.specifyNetwork(), subCategories: { gbif: { title: localized('GBIF'), items: { - publishingOrganization: definePref({ + // Names chosen to match SpecifyNetworkCollection components + publishingOrg: definePref({ title: localized('Publishing organization key'), requiresReload: false, visible: true, defaultValue: undefined, type: 'java.lang.String', }), - collectionKey: definePref({ + datasetKey: definePref({ title: localized('Collection key'), requiresReload: false, visible: true, @@ -203,6 +234,7 @@ export const collectionPreferenceDefinitions = { }, }, }, + catalogNumberParentInheritance: { title: queryText.catalogNumberParentCOInheritance(), description: localized( @@ -223,6 +255,7 @@ export const collectionPreferenceDefinitions = { }, }, }, + uniqueCatalogNumberAccrossComponentAndCO: { title: queryText.uniqueCatalogNumberAcrossComponentAndCo(), subCategories: { @@ -244,4 +277,5 @@ export const collectionPreferenceDefinitions = { }, } as const; +// Keep the same type-assert pattern as UserDefinitions ensure()(collectionPreferenceDefinitions); From 70ea53491ddce792755417ccd8e44a7c30f974c8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 3 Oct 2025 23:00:02 -0400 Subject: [PATCH 008/100] fix TypeScript errors in collection preference definitions --- .../components/Preferences/CollectionDefinitions.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 9655e329e19..1081a1ed6ac 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -25,9 +25,9 @@ export const collectionPreferenceDefinitions = { (localized('Pick lists') as LocalizedString), items: { sp7_scope_table_picklists: definePref({ - title: localized('Scope “Entire Table” picklists'), + title: localized('Scope "Entire Table" picklists'), description: localized( - 'Restrict “Entire Table” picklists to values used by records in this collection.' + 'Restrict "Entire Table" picklists to values used by records in this collection.' ), requiresReload: false, visible: true, @@ -131,8 +131,7 @@ export const collectionPreferenceDefinitions = { items: { display: definePref<{ readonly basicView: RA; - // detailedView shape isn’t used here; keep it permissive - readonly detailedView: unknown; + readonly detailedView: Record; }>({ title: preferencesText.displayBasicView(), requiresReload: false, @@ -194,7 +193,6 @@ export const collectionPreferenceDefinitions = { gbif: { title: localized('GBIF'), items: { - // Names chosen to match SpecifyNetworkCollection components publishingOrg: definePref({ title: localized('Publishing organization key'), requiresReload: false, @@ -277,5 +275,4 @@ export const collectionPreferenceDefinitions = { }, } as const; -// Keep the same type-assert pattern as UserDefinitions -ensure()(collectionPreferenceDefinitions); +ensure()(collectionPreferenceDefinitions); \ No newline at end of file From 751fcc5228419606bf54aaee30a2e2d28c74e270 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 4 Oct 2025 11:46:50 -0400 Subject: [PATCH 009/100] align CollectionDefinitions with UserDefinitions typing --- .../Preferences/CollectionDefinitions.tsx | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 1081a1ed6ac..9ac2b95a503 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -15,6 +15,23 @@ import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; +const specifyNetworkItems = { + publishingOrganization: definePref({ + title: localized('Publishing organization key'), + requiresReload: false, + visible: true, + defaultValue: undefined, + type: 'java.lang.String', + }), + collectionKey: definePref({ + title: localized('Collection key'), + requiresReload: false, + visible: true, + defaultValue: undefined, + type: 'java.lang.String', + }), +} as const; + export const collectionPreferenceDefinitions = { general: { title: preferencesText.general(), @@ -131,14 +148,14 @@ export const collectionPreferenceDefinitions = { items: { display: definePref<{ readonly basicView: RA; - readonly detailedView: Record; + readonly detailedView: RA; }>({ title: preferencesText.displayBasicView(), requiresReload: false, visible: false, defaultValue: { basicView: [], - detailedView: {}, + detailedView: [], }, renderer: f.never, container: 'label', @@ -184,6 +201,10 @@ export const collectionPreferenceDefinitions = { }), }, }, + specifyNetwork: { + title: specifyNetworkText.specifyNetwork(), + items: specifyNetworkItems, + }, }, }, @@ -192,22 +213,7 @@ export const collectionPreferenceDefinitions = { subCategories: { gbif: { title: localized('GBIF'), - items: { - publishingOrg: definePref({ - title: localized('Publishing organization key'), - requiresReload: false, - visible: true, - defaultValue: undefined, - type: 'java.lang.String', - }), - datasetKey: definePref({ - title: localized('Collection key'), - requiresReload: false, - visible: true, - defaultValue: undefined, - type: 'java.lang.String', - }), - }, + items: specifyNetworkItems, }, }, }, @@ -275,4 +281,4 @@ export const collectionPreferenceDefinitions = { }, } as const; -ensure()(collectionPreferenceDefinitions); \ No newline at end of file +ensure()(collectionPreferenceDefinitions); From 20b207e0bd6648d0fd9991f97a1cca5eb0144998 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 4 Oct 2025 12:05:53 -0400 Subject: [PATCH 010/100] Fix localization scanner issues in preferences --- .../lib/components/Preferences/CollectionDefinitions.tsx | 5 +---- .../lib/components/Preferences/CollectionPreferencesPage.tsx | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 9ac2b95a503..ab894b89a70 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -1,7 +1,6 @@ /** * Definitions for Collection preferences */ -import type { LocalizedString } from 'typesafe-i18n'; import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; @@ -37,9 +36,7 @@ export const collectionPreferenceDefinitions = { title: preferencesText.general(), subCategories: { pickLists: { - title: - preferencesText.filterPickLists?.() ?? - (localized('Pick lists') as LocalizedString), + title: preferencesText.filterPickLists(), items: { sp7_scope_table_picklists: definePref({ title: localized('Scope "Entire Table" picklists'), diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx index 951bda72546..5eddc5b6626 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx @@ -249,9 +249,7 @@ function CollectionPreferenceItem({ <> {item.description ? ' ' : null} - {headerText.documentation - ? headerText.documentation() - : ('Documentation' as LocalizedString)} + {headerText.documentation()} )} From 5e1ab0165deb9e2c4a39751c10aa0a6bca607f06 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 4 Oct 2025 16:10:26 +0000 Subject: [PATCH 011/100] Lint code with ESLint and Prettier Triggered by 20b207e0bd6648d0fd9991f97a1cca5eb0144998 on branch refs/heads/issue-7440 --- .../Preferences/CollectionPreferencesPage.tsx | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx index 5eddc5b6626..d5458ca9dd5 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx @@ -7,8 +7,8 @@ import type { LocalizedString } from 'typesafe-i18n'; import { usePromise } from '../../hooks/useAsyncState'; import { commonText } from '../../localization/common'; -import { preferencesText } from '../../localization/preferences'; import { headerText } from '../../localization/header'; +import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import { Container, Key } from '../Atoms'; @@ -17,11 +17,10 @@ import { className } from '../Atoms/className'; import { Link } from '../Atoms/Link'; import { ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; -import { ProtectedTool } from '../Permissions/PermissionDenied'; import { hasPermission } from '../Permissions/helpers'; - -import { collectionPreferences } from './collectionPreferences'; +import { ProtectedTool } from '../Permissions/PermissionDenied'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; +import { collectionPreferences } from './collectionPreferences'; import { DefaultPreferenceItemRender } from './Renderers'; import type { GenericPreferences, PreferenceItem } from './types'; @@ -92,8 +91,7 @@ export function CollectionPreferencesContent(): JSX.Element { return (
{definitions.map( - ( - [category, { title, description = undefined, subCategories }]) => ( + ([category, { title, description = undefined, subCategories }]) => ( ( ))} @@ -200,7 +198,7 @@ function CollectionPreferenceItem({ 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; // Minimal doc link mapping - const docHref: string | undefined = (() => { + const documentHref: string | undefined = (() => { if (name === 'sp7_scope_table_picklists') return DOCS.picklists; if (name === 'attachment.is_public_default') return DOCS.attachments; if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) @@ -234,7 +232,7 @@ function CollectionPreferenceItem({ />

- {(item.description !== undefined || docHref) && ( + {(item.description !== undefined || documentHref) && (

{item.description !== undefined && ( )} - {docHref && ( + {documentHref && ( <> {item.description ? ' ' : null} - + {headerText.documentation()} @@ -261,7 +259,7 @@ function CollectionPreferenceItem({ } + definition={item} item={name} subcategory={subcategory} value={value} From 91644ab8e3270ace5345da7e60ec3cbc1a7321b8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 4 Oct 2025 21:47:39 -0400 Subject: [PATCH 012/100] adding path for role gated access --- .../lib/components/Header/userToolDefinitions.ts | 6 ++++++ .../components/Preferences/CollectionDefinitions.tsx | 12 +----------- .../frontend/js_src/lib/components/Router/Routes.tsx | 8 ++++++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index 6855811b3b4..ef262b42e9f 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -57,6 +57,12 @@ const rawUserTools = ensure>>>()({ url: '/specify/user-preferences/', icon: icons.cog, }, + collectionPreferences: { + title: preferencesText.collectionPreferences(), + url: '/specify/collection-preferences/', + icon: icons.collection, + enabled: () => hasToolPermission('resources', 'update'), + }, schemaConfig: { title: schemaText.schemaConfig(), url: '/specify/schema-config/', diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index ab894b89a70..762cea9723c 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -155,7 +155,7 @@ export const collectionPreferenceDefinitions = { detailedView: [], }, renderer: f.never, - container: 'label', + container: 'div', }), }, }, @@ -205,16 +205,6 @@ export const collectionPreferenceDefinitions = { }, }, - specifyNetwork: { - title: specifyNetworkText.specifyNetwork(), - subCategories: { - gbif: { - title: localized('GBIF'), - items: specifyNetworkItems, - }, - }, - }, - catalogNumberInheritance: { title: queryText.catalogNumberInheritance(), description: localized( diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index e932a03d020..678636cbaac 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -376,6 +376,14 @@ export const routes: RA = [ ({ PreferencesWrapper }) => PreferencesWrapper ), }, + { + path: 'collection-preferences', + title: preferencesText.collectionPreferences(), + element: () => + import('../Preferences/CollectionPreferencesPage').then( + ({ CollectionPreferencesWrapper }) => CollectionPreferencesWrapper + ), + }, { path: 'schema-config', title: schemaText.schemaConfig(), From 78c6e544302cc9a05cc53aece3e7c53295e17944 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 6 Oct 2025 17:17:11 -0400 Subject: [PATCH 013/100] Refactor collection preferences renderer and documentation link layout --- .../Preferences/CollectionPreferencesPage.tsx | 314 ------------------ .../lib/components/Preferences/Editor.tsx | 5 +- .../lib/components/Preferences/index.tsx | 170 ++++++++-- .../js_src/lib/components/Router/Routes.tsx | 2 +- 4 files changed, 140 insertions(+), 351 deletions(-) delete mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx deleted file mode 100644 index d5458ca9dd5..00000000000 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionPreferencesPage.tsx +++ /dev/null @@ -1,314 +0,0 @@ -/** - * Edit collection preferences - */ - -import React from 'react'; -import type { LocalizedString } from 'typesafe-i18n'; - -import { usePromise } from '../../hooks/useAsyncState'; -import { commonText } from '../../localization/common'; -import { headerText } from '../../localization/header'; -import { preferencesText } from '../../localization/preferences'; -import { StringToJsx } from '../../localization/utils'; -import { f } from '../../utils/functools'; -import { Container, Key } from '../Atoms'; -import { Button } from '../Atoms/Button'; -import { className } from '../Atoms/className'; -import { Link } from '../Atoms/Link'; -import { ReadOnlyContext } from '../Core/Contexts'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; -import { hasPermission } from '../Permissions/helpers'; -import { ProtectedTool } from '../Permissions/PermissionDenied'; -import { collectionPreferenceDefinitions } from './CollectionDefinitions'; -import { collectionPreferences } from './collectionPreferences'; -import { DefaultPreferenceItemRender } from './Renderers'; -import type { GenericPreferences, PreferenceItem } from './types'; - -const preferencesPromise = Promise.all([collectionPreferences.fetch()]).then( - f.true -); - -const DOCS = { - picklists: - 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562', - attachments: - 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640', - trees: - 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', - stats: 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', - specifyNetwork: - 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', - catalogNumbers: - 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859', -} as const; - -export function useCollectionPrefDefinitions() { - const visibilityContext = React.useMemo( - () => ({ isDarkMode: false, isRedirecting: false }), - [] - ); - return React.useMemo( - () => - Object.entries(collectionPreferenceDefinitions as GenericPreferences) - .map( - ([category, { subCategories, ...categoryData }]) => - [ - category, - { - ...categoryData, - subCategories: Object.entries(subCategories) - .map( - ([subCategory, { items, ...subCategoryData }]) => - [ - subCategory, - { - ...subCategoryData, - items: Object.entries(items).filter( - ([_name, { visible }]) => - typeof visible === 'function' - ? visible(visibilityContext) - : visible !== false - ), - }, - ] as const - ) - .filter(([_name, { items }]) => items.length > 0), - }, - ] as const - ) - .filter(([_name, { subCategories }]) => subCategories.length > 0), - [visibilityContext] - ); -} - -type ItemEntry = ReturnType< - typeof useCollectionPrefDefinitions ->[number][1]['subCategories'][number][1]['items'][number]; - -export function CollectionPreferencesContent(): JSX.Element { - const definitions = useCollectionPrefDefinitions(); - - return ( -

- {definitions.map( - ([category, { title, description = undefined, subCategories }]) => ( - - -

- {typeof title === 'function' ? title() : title} -

- - {description !== undefined && ( -

- {typeof description === 'function' - ? description() - : description} -

- )} - - {subCategories.map( - ([subcategory, { title, description: subDesc, items }]) => ( -
-
-

- {typeof title === 'function' ? title() : title} -

- -
- - items.forEach(([name]) => { - const def = collectionPreferences.definition( - category as never, - subcategory as never, - name as never - ); - collectionPreferences.set( - category as never, - subcategory as never, - name as never, - (def as { readonly defaultValue: unknown }) - .defaultValue as never - ); - }) - } - > - {commonText.reset()} - -
-
- - {subDesc !== undefined && ( -

- {typeof subDesc === 'function' ? subDesc() : subDesc} -

- )} - - {items.map((entry) => ( - - ))} -
- ) - )} -
-
- ) - )} -
- ); -} - -function CollectionPreferenceItem({ - category, - subcategory, - itemEntry, -}: { - readonly category: string; - readonly subcategory: string; - readonly itemEntry: ItemEntry; -}): JSX.Element { - const isReadOnly = React.useContext(ReadOnlyContext); - const [name, item] = itemEntry; - - const canEdit = - !isReadOnly && - (item.visible !== 'protected' || - hasPermission('/preferences/user', 'edit_protected')); - - const [value, setValue] = collectionPreferences.use( - category as never, - subcategory as never, - name as never - ); - - const Renderer = - 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; - - // Minimal doc link mapping - const documentHref: string | undefined = (() => { - if (name === 'sp7_scope_table_picklists') return DOCS.picklists; - if (name === 'attachment.is_public_default') return DOCS.attachments; - if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) - return DOCS.trees; - if (name === 'showPreparationsTotal' || name === 'refreshRate') - return DOCS.stats; - if (name === 'publishingOrganization' || name === 'collectionKey') - return DOCS.specifyNetwork; - if (category.startsWith('catalogNumber')) return DOCS.catalogNumbers; - return undefined; - })(); - - const wrapperProps = { - className: ` - flex items-start gap-2 md:flex-row flex-col - ${canEdit ? '' : '!cursor-not-allowed'} - `, - title: canEdit ? undefined : preferencesText.adminsOnlyPreference(), - } as const; - - const content = ( - <> -
-

- -

- - {(item.description !== undefined || documentHref) && ( -

- {item.description !== undefined && ( - - )} - {documentHref && ( - <> - {item.description ? ' ' : null} - - {headerText.documentation()} - - - )} -

- )} -
- -
- - - -
- - ); - - return 'container' in item && item.container === 'div' ? ( -
{content}
- ) : ( - - ); -} - -function FormatString({ - text, -}: { - readonly text: JSX.Element | LocalizedString; -}): JSX.Element { - return typeof text === 'object' ? ( - text - ) : text.includes('') ? ( - - {key}, - }} - string={text} - /> - - ) : ( - <>{text} - ); -} - -function CollectionPreferences(): JSX.Element { - return ( - -
- -
-
- ); -} - -export function CollectionPreferencesWrapper(): JSX.Element | null { - const [hasFetched] = usePromise(preferencesPromise, true); - return hasFetched === true ? : null; -} diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 45e3cbed8fb..601af11c2b5 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -6,7 +6,6 @@ import { PreferencesContent } from '../Preferences'; import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; -import { CollectionPreferencesContent } from './CollectionPreferencesPage'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; @@ -72,7 +71,7 @@ export function CollectionPreferencesEditor({ return ( - + ); -} \ No newline at end of file +} diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 64faa2c0d8f..3244f3a3051 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -9,6 +9,7 @@ import type { LocalizedString } from 'typesafe-i18n'; import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; +import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; @@ -22,6 +23,8 @@ import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission } from '../Permissions/helpers'; import { PreferencesAside } from './Aside'; +import type { BasePreferences } from './BasePreferences'; +import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; import { useDarkMode } from './Hooks'; import { DefaultPreferenceItemRender } from './Renderers'; @@ -29,6 +32,59 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; +import { ProtectedTool } from '../Permissions/PermissionDenied'; + +const DOCS = { + picklists: + 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562', + attachments: + 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640', + trees: + 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', + stats: 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + specifyNetwork: + 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', + catalogNumbers: + 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859', +} as const; + +const preferenceInstances = { + user: userPreferences as BasePreferences, + collection: collectionPreferences as BasePreferences, +} as const; + +const preferenceDefinitions = { + user: userPreferenceDefinitions as GenericPreferences, + collection: collectionPreferenceDefinitions as GenericPreferences, +} as const; + +const resolveCollectionDocHref = ( + category: string, + _subcategory: string, + name: string +): string | undefined => { + if (name === 'sp7_scope_table_picklists') return DOCS.picklists; + if (name === 'attachment.is_public_default') return DOCS.attachments; + if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) + return DOCS.trees; + if (name === 'showPreparationsTotal' || name === 'refreshRate') + return DOCS.stats; + if (name === 'publishingOrganization' || name === 'collectionKey') + return DOCS.specifyNetwork; + if (category.startsWith('catalogNumber')) return DOCS.catalogNumbers; + return undefined; +}; + +const docHrefResolvers = { + user: undefined, + collection: resolveCollectionDocHref, +} as const; + +export type PreferenceType = keyof typeof preferenceInstances; + +const collectionPreferencesPromise = Promise.all([ + collectionPreferences.fetch(), +]).then(f.true); /** * Fetch app resource that stores current user preferences @@ -110,10 +166,10 @@ function Preferences(): JSX.Element { } /** Hide invisible preferences. Remote empty categories and subCategories */ -export function usePrefDefinitions() { +export function usePrefDefinitions(prefType: PreferenceType = 'user') { const isDarkMode = useDarkMode(); const isRedirecting = React.useContext(userPreferences.Context) !== undefined; - const preferencesVisibilityContext = React.useMemo( + const userVisibilityContext = React.useMemo( () => ({ isDarkMode, isRedirecting, @@ -121,9 +177,19 @@ export function usePrefDefinitions() { [isDarkMode, isRedirecting] ); + const collectionVisibilityContext = React.useMemo( + () => ({ isDarkMode: false, isRedirecting: false }), + [] + ); + + const visibilityContext = + prefType === 'user' ? userVisibilityContext : collectionVisibilityContext; + + const definitions = preferenceDefinitions[prefType]; + return React.useMemo( () => - Object.entries(userPreferenceDefinitions as GenericPreferences) + Object.entries(definitions) .map( ([category, { subCategories, ...categoryData }]) => [ @@ -140,7 +206,7 @@ export function usePrefDefinitions() { items: Object.entries(items).filter( ([_name, { visible }]) => typeof visible === 'function' - ? visible(preferencesVisibilityContext) + ? visible(visibilityContext) : visible !== false ), }, @@ -151,17 +217,21 @@ export function usePrefDefinitions() { ] as const ) .filter(([_name, { subCategories }]) => subCategories.length > 0), - [preferencesVisibilityContext] + [definitions, visibilityContext] ); } export function PreferencesContent({ forwardRefs, + prefType = 'user', }: { readonly forwardRefs?: (index: number, element: HTMLElement | null) => void; + readonly prefType?: PreferenceType; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); - const definitions = usePrefDefinitions(); + const definitions = usePrefDefinitions(prefType); + const preferences = preferenceInstances[prefType]; + const resolveDocHref = docHrefResolvers[prefType]; return (
{definitions.map( @@ -201,19 +271,16 @@ export function PreferencesContent({ items.forEach(([name]) => { - userPreferences.set( - category as 'general', - subcategory as 'ui', - name as 'theme', - /* - * Need to get default value via this - * function as defaults may be changed - */ - userPreferences.definition( - category as 'general', - subcategory as 'ui', - name as 'theme' - ).defaultValue + const definition = preferences.definition( + category as never, + subcategory as never, + name as never + ); + preferences.set( + category as never, + subcategory as never, + name as never, + definition.defaultValue as never ); }) } @@ -234,6 +301,9 @@ export function PreferencesContent({ !isReadOnly && (item.visible !== 'protected' || hasPermission('/preferences/user', 'edit_protected')); + const docHref = resolveDocHref?.(category, subcategory, name); + const stackDocumentation = + prefType === 'collection' && docHref !== undefined; const props = { className: ` flex items-start gap-2 md:flex-row flex-col @@ -261,15 +331,32 @@ export function PreferencesContent({ } />

- {item.description !== undefined && ( -

- + {(item.description !== undefined || + docHref !== undefined) && ( +

+ {item.description !== undefined && ( + + )} + {docHref !== undefined && ( + + {headerText.documentation()} + + )}

)}
@@ -284,6 +371,7 @@ export function PreferencesContent({ category={category} item={item} name={name} + preferences={preferences} subcategory={subcategory} /> @@ -333,19 +421,20 @@ function Item({ category, subcategory, name, + preferences, }: { readonly item: PreferenceItem; readonly category: string; readonly subcategory: string; readonly name: string; + readonly preferences: BasePreferences; }): JSX.Element { const Renderer = 'renderer' in item ? item.renderer : DefaultPreferenceItemRender; - const [value, setValue] = userPreferences.use( - // Asserting types just to simplify typing - category as 'general', - subcategory as 'ui', - name as 'theme' + const [value, setValue] = preferences.use( + category as never, + subcategory as never, + name as never ); const children = ( : null; } + +function CollectionPreferences(): JSX.Element { + return ( + +
+ +
+
+ ); +} + +export function CollectionPreferencesWrapper(): JSX.Element | null { + const [hasFetched] = usePromise(collectionPreferencesPromise, true); + return hasFetched === true ? : null; +} diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index 678636cbaac..5ddcf520692 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -380,7 +380,7 @@ export const routes: RA = [ path: 'collection-preferences', title: preferencesText.collectionPreferences(), element: () => - import('../Preferences/CollectionPreferencesPage').then( + import('../Preferences').then( ({ CollectionPreferencesWrapper }) => CollectionPreferencesWrapper ), }, From f6c0493ec38cd8558fd0e861d803c54aef043948 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 6 Oct 2025 21:21:50 +0000 Subject: [PATCH 014/100] Lint code with ESLint and Prettier Triggered by 78c6e544302cc9a05cc53aece3e7c53295e17944 on branch refs/heads/issue-7440 --- .../Preferences/CollectionDefinitions.tsx | 4 ++- .../lib/components/Preferences/Editor.tsx | 14 +++++---- .../lib/components/Preferences/index.tsx | 30 ++++++++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 762cea9723c..ef96f818cc1 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -117,7 +117,9 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Storage': definePref({ - title: localized('Allow children under synonymized Storage nodes'), + title: localized( + 'Allow children under synonymized Storage nodes' + ), requiresReload: false, visible: true, defaultValue: false, diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 601af11c2b5..5a761caa2b2 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -49,7 +49,7 @@ export function CollectionPreferencesEditor({ }: AppResourceTabProps): JSX.Element { const [preferencesInstance] = useLiveState( React.useCallback(() => { - const tmpCollectionPrefs = new BasePreferences({ + const temporaryCollectionPrefs = new BasePreferences({ definitions: collectionPreferenceDefinitions, values: { resourceName: 'CollectionPreferences', @@ -57,14 +57,16 @@ export function CollectionPreferencesEditor({ }, defaultValues: undefined, developmentGlobal: '_editingCollectionPreferences', - syncChanges: false, + syncChanges: false, }); - tmpCollectionPrefs.setRaw(JSON.parse(!data || data.length === 0 ? '{}' : data)); - tmpCollectionPrefs.events.on('update', () => - onChange(JSON.stringify(tmpCollectionPrefs.getRaw())) + temporaryCollectionPrefs.setRaw( + JSON.parse(!data || data.length === 0 ? '{}' : data) + ); + temporaryCollectionPrefs.events.on('update', () => + onChange(JSON.stringify(temporaryCollectionPrefs.getRaw())) ); - return tmpCollectionPrefs; + return temporaryCollectionPrefs; }, [data, onChange]) ); const Context = collectionPreferences.Context; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 3244f3a3051..8c127b5c940 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -22,6 +22,7 @@ import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission } from '../Permissions/helpers'; +import { ProtectedTool } from '../Permissions/PermissionDenied'; import { PreferencesAside } from './Aside'; import type { BasePreferences } from './BasePreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; @@ -32,7 +33,6 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; -import { ProtectedTool } from '../Permissions/PermissionDenied'; const DOCS = { picklists: @@ -58,7 +58,7 @@ const preferenceDefinitions = { collection: collectionPreferenceDefinitions as GenericPreferences, } as const; -const resolveCollectionDocHref = ( +const resolveCollectionDocumentHref = ( category: string, _subcategory: string, name: string @@ -75,9 +75,9 @@ const resolveCollectionDocHref = ( return undefined; }; -const docHrefResolvers = { +const documentHrefResolvers = { user: undefined, - collection: resolveCollectionDocHref, + collection: resolveCollectionDocumentHref, } as const; export type PreferenceType = keyof typeof preferenceInstances; @@ -231,7 +231,7 @@ export function PreferencesContent({ const isReadOnly = React.useContext(ReadOnlyContext); const definitions = usePrefDefinitions(prefType); const preferences = preferenceInstances[prefType]; - const resolveDocHref = docHrefResolvers[prefType]; + const resolveDocumentHref = documentHrefResolvers[prefType]; return (
{definitions.map( @@ -301,9 +301,13 @@ export function PreferencesContent({ !isReadOnly && (item.visible !== 'protected' || hasPermission('/preferences/user', 'edit_protected')); - const docHref = resolveDocHref?.(category, subcategory, name); + const documentHref = resolveDocumentHref?.( + category, + subcategory, + name + ); const stackDocumentation = - prefType === 'collection' && docHref !== undefined; + prefType === 'collection' && documentHref !== undefined; const props = { className: ` flex items-start gap-2 md:flex-row flex-col @@ -332,7 +336,7 @@ export function PreferencesContent({ />

{(item.description !== undefined || - docHref !== undefined) && ( + documentHref !== undefined) && (

)} - {docHref !== undefined && ( + {documentHref !== undefined && ( {headerText.documentation()} From c4ddea8efb09522d45738b84f5587708f324b757 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 6 Oct 2025 18:22:00 -0400 Subject: [PATCH 015/100] Localization cleanup for collection preferences --- .../Preferences/CollectionDefinitions.tsx | 71 ++++++---------- .../js_src/lib/localization/attachments.ts | 18 +++++ .../js_src/lib/localization/preferences.ts | 26 +++++- .../js_src/lib/localization/specifyNetwork.ts | 18 +++++ .../js_src/lib/localization/stats.tsx | 15 ++++ .../frontend/js_src/lib/localization/tree.ts | 81 +++++++++++++++++++ 6 files changed, 181 insertions(+), 48 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index ef96f818cc1..04b5dc5066e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -6,24 +6,25 @@ import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; import { specifyNetworkText } from '../../localization/specifyNetwork'; +import { treeText } from '../../localization/tree'; import { statsText } from '../../localization/stats'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; -import { ensure, localized } from '../../utils/types'; +import { ensure } from '../../utils/types'; import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; const specifyNetworkItems = { publishingOrganization: definePref({ - title: localized('Publishing organization key'), + title: specifyNetworkText.publishingOrganizationKey(), requiresReload: false, visible: true, defaultValue: undefined, type: 'java.lang.String', }), collectionKey: definePref({ - title: localized('Collection key'), + title: specifyNetworkText.collectionKey(), requiresReload: false, visible: true, defaultValue: undefined, @@ -39,10 +40,8 @@ export const collectionPreferenceDefinitions = { title: preferencesText.filterPickLists(), items: { sp7_scope_table_picklists: definePref({ - title: localized('Scope "Entire Table" picklists'), - description: localized( - 'Restrict "Entire Table" picklists to values used by records in this collection.' - ), + title: preferencesText.scopeEntireTablePicklists(), + description: preferencesText.scopeEntireTablePicklistsDescription(), requiresReload: false, visible: true, defaultValue: false, @@ -54,10 +53,8 @@ export const collectionPreferenceDefinitions = { title: attachmentsText.attachments(), items: { 'attachment.is_public_default': definePref({ - title: localized('New attachments are public'), - description: localized( - 'Set the default visibility for attachments created within this collection.' - ), + title: attachmentsText.publicDefault(), + description: attachmentsText.publicDefaultDescription(), requiresReload: false, visible: true, defaultValue: false, @@ -69,19 +66,15 @@ export const collectionPreferenceDefinitions = { }, treeManagement: { - title: localized('Tree management'), + title: treeText.treeManagement(), subCategories: { synonymized: { - title: localized('Synonymized nodes'), - description: localized( - 'Allow creating children under synonymized nodes in specific trees.' - ), + title: treeText.synonymizedNodes(), + description: treeText.synonymizedNodesDescription(), items: { 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': definePref({ - title: localized( - 'Allow children under synonymized Geologic Time Period nodes' - ), + title: treeText.allowSynonymizedGeologicTimePeriodChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -89,7 +82,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Taxon': definePref({ - title: localized('Allow children under synonymized Taxon nodes'), + title: treeText.allowSynonymizedTaxonChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -97,9 +90,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Geography': definePref({ - title: localized( - 'Allow children under synonymized Geography nodes' - ), + title: treeText.allowSynonymizedGeographyChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -107,9 +98,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.LithoStrat': definePref({ - title: localized( - 'Allow children under synonymized Lithostratigraphy nodes' - ), + title: treeText.allowSynonymizedLithostratChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -117,9 +106,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Storage': definePref({ - title: localized( - 'Allow children under synonymized Storage nodes' - ), + title: treeText.allowSynonymizedStorageChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -127,9 +114,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.TectonicUnit': definePref({ - title: localized( - 'Allow children under synonymized Tectonic Unit nodes' - ), + title: treeText.allowSynonymizedTectonicUnitChildren(), requiresReload: false, visible: true, defaultValue: false, @@ -171,7 +156,7 @@ export const collectionPreferenceDefinitions = { title: preferencesText.appearance(), items: { layout: definePref | undefined>({ - title: localized('_Defines the layout of the stats page'), + title: statsText.layoutPreference(), requiresReload: false, visible: false, defaultValue: undefined, @@ -179,20 +164,16 @@ export const collectionPreferenceDefinitions = { container: 'label', }), showPreparationsTotal: definePref({ - title: localized('Show preparation totals'), - description: localized( - 'Include an overall total across preparation types on the statistics page.' - ), + title: statsText.showPreparationsTotal(), + description: statsText.showPreparationsTotalDescription(), requiresReload: false, visible: true, defaultValue: true, type: 'java.lang.Boolean', }), refreshRate: definePref({ - title: localized('Auto-refresh rate (hours)'), - description: localized( - 'Specify how frequently shared statistics refresh their data.' - ), + title: statsText.autoRefreshRate(), + description: statsText.autoRefreshRateDescription(), requiresReload: false, visible: true, defaultValue: 24, @@ -209,9 +190,7 @@ export const collectionPreferenceDefinitions = { catalogNumberInheritance: { title: queryText.catalogNumberInheritance(), - description: localized( - 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.' - ), + description: preferencesText.catalogNumberInheritanceDescription(), subCategories: { behavior: { title: preferencesText.behavior(), @@ -230,9 +209,7 @@ export const collectionPreferenceDefinitions = { catalogNumberParentInheritance: { title: queryText.catalogNumberParentCOInheritance(), - description: localized( - 'Control whether component records inherit catalog numbers from their parent Collection Object.' - ), + description: preferencesText.catalogNumberParentInheritanceDescription(), subCategories: { behavior: { title: preferencesText.behavior(), diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index 421e658e11d..59133101931 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -677,4 +677,22 @@ export const attachmentsText = createDictionary({ 'ru-ru': 'Удаление вложения', 'uk-ua': 'Видалення вкладень', }, + publicDefault: { + 'en-us': 'New attachments are public', + 'ru-ru': 'New attachments are public', + 'es-es': 'New attachments are public', + 'fr-fr': 'New attachments are public', + 'uk-ua': 'New attachments are public', + 'de-ch': 'New attachments are public', + 'pt-br': 'New attachments are public', + }, + publicDefaultDescription: { + 'en-us': 'Set the default visibility for attachments created within this collection.', + 'ru-ru': 'Set the default visibility for attachments created within this collection.', + 'es-es': 'Set the default visibility for attachments created within this collection.', + 'fr-fr': 'Set the default visibility for attachments created within this collection.', + 'uk-ua': 'Set the default visibility for attachments created within this collection.', + 'de-ch': 'Set the default visibility for attachments created within this collection.', + 'pt-br': 'Set the default visibility for attachments created within this collection.', + }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 0a802fd5cd7..d7e3e453d2f 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1283,6 +1283,30 @@ export const preferencesText = createDictionary({ 'de-ch': 'Schließen durch Klicken von außen', 'pt-br': 'Fechar com clique externo', }, + scopeEntireTablePicklists: { + 'en-us': 'Scope "Entire Table" picklists', + 'ru-ru': 'Scope "Entire Table" picklists', + 'es-es': 'Scope "Entire Table" picklists', + 'fr-fr': 'Scope "Entire Table" picklists', + 'uk-ua': 'Scope "Entire Table" picklists', + 'de-ch': 'Scope "Entire Table" picklists', + 'pt-br': 'Scope "Entire Table" picklists', + }, + scopeEntireTablePicklistsDescription: { + 'en-us': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'ru-ru': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'es-es': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'fr-fr': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'uk-ua': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'de-ch': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'pt-br': 'Restrict "Entire Table" picklists to values used by records in this collection.', + }, + catalogNumberInheritanceDescription:{ + 'en-us': 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.', + }, + catalogNumberParentInheritanceDescription:{ + 'en-us': 'Control whether component records inherit catalog numbers from their parent Collection Object.' + }, specifyNetworkBadge: { 'en-us': 'Specify Network Badge', 'ru-ru': 'Укажите сетевой значок', @@ -1292,7 +1316,7 @@ export const preferencesText = createDictionary({ 'de-ch': 'Netzwerk-Badge angeben', 'pt-br': 'Especificar emblema de rede', }, - useAccessibleFullDatePicker: { + useAccessibleFullDatePicker: { 'en-us': 'Use accessible full date picker', 'ru-ru': 'Используйте доступный полный выбор даты', 'es-es': 'Utilice el selector de fecha completo y accesible', diff --git a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts index 8af7cf05095..8fec68d009c 100644 --- a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts +++ b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts @@ -16,6 +16,24 @@ export const specifyNetworkText = createDictionary({ 'de-ch': 'Specify Network', 'pt-br': 'Especificar rede', }, + publishingOrganizationKey: { + 'en-us': 'Publishing organization key', + 'ru-ru': 'Publishing organization key', + 'es-es': 'Publishing organization key', + 'fr-fr': "Clé d’organisation de publication", + 'uk-ua': 'Publishing organization key', + 'de-ch': 'Publishing organization key', + 'pt-br': 'Chave da organização publicadora', + }, + collectionKey: { + 'en-us': 'Collection key', + 'ru-ru': 'Collection key', + 'es-es': 'Collection key', + 'fr-fr': 'Identifiant de collection', + 'uk-ua': 'Collection key', + 'de-ch': 'Collection key', + 'pt-br': 'Chave da coleção', + }, occurrenceOrGuidRequired: { 'en-us': 'Species Name or GUID must be provided to display this page', 'de-ch': diff --git a/specifyweb/frontend/js_src/lib/localization/stats.tsx b/specifyweb/frontend/js_src/lib/localization/stats.tsx index d7e726cd65d..bf901980c06 100644 --- a/specifyweb/frontend/js_src/lib/localization/stats.tsx +++ b/specifyweb/frontend/js_src/lib/localization/stats.tsx @@ -313,5 +313,20 @@ export const statsText = createDictionary({ categoryToDelete: { 'en-us': 'This will permanently delete the following category', }, + layoutPreference: { + 'en-us': 'Defines the layout of the statistics page', + }, + showPreparationsTotal: { + 'en-us': 'Show preparation totals', + }, + showPreparationsTotalDescription: { + 'en-us': 'Include an overall total across preparation types on the statistics page.', + }, + autoRefreshRate: { + 'en-us': 'Auto-refresh rate (hours)', + }, + autoRefreshRateDescription: { + 'en-us': 'Specify how frequently shared statistics refresh their data.', + }, }); /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 057933aa2f1..f362d2bdd73 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -703,4 +703,85 @@ export const treeText = createDictionary({ 'ru-ru': 'Метеориты', 'uk-ua': 'Метеорити', }, + treeManagement: { + 'en-us': 'Tree management', + 'ru-ru': 'Tree management', + 'es-es': 'Tree management', + 'fr-fr': 'Tree management', + 'uk-ua': 'Tree management', + 'de-ch': 'Tree management', + 'pt-br': 'Tree management', + }, + synonymizedNodes: { + 'en-us': 'Synonymized nodes', + 'ru-ru': 'Synonymized nodes', + 'es-es': 'Synonymized nodes', + 'fr-fr': 'Synonymized nodes', + 'uk-ua': 'Synonymized nodes', + 'de-ch': 'Synonymized nodes', + 'pt-br': 'Synonymized nodes', + }, + synonymizedNodesDescription: { + 'en-us': 'Allow creating children under synonymized nodes in specific trees.', + 'ru-ru': 'Allow creating children under synonymized nodes in specific trees.', + 'es-es': 'Allow creating children under synonymized nodes in specific trees.', + 'fr-fr': 'Allow creating children under synonymized nodes in specific trees.', + 'uk-ua': 'Allow creating children under synonymized nodes in specific trees.', + 'de-ch': 'Allow creating children under synonymized nodes in specific trees.', + 'pt-br': 'Allow creating children under synonymized nodes in specific trees.', + }, + allowSynonymizedGeologicTimePeriodChildren: { + 'en-us': 'Allow children under synonymized Geologic Time Period nodes', + 'ru-ru': 'Allow children under synonymized Geologic Time Period nodes', + 'es-es': 'Allow children under synonymized Geologic Time Period nodes', + 'fr-fr': 'Allow children under synonymized Geologic Time Period nodes', + 'uk-ua': 'Allow children under synonymized Geologic Time Period nodes', + 'de-ch': 'Allow children under synonymized Geologic Time Period nodes', + 'pt-br': 'Allow children under synonymized Geologic Time Period nodes', + }, + allowSynonymizedTaxonChildren: { + 'en-us': 'Allow children under synonymized Taxon nodes', + 'ru-ru': 'Allow children under synonymized Taxon nodes', + 'es-es': 'Allow children under synonymized Taxon nodes', + 'fr-fr': 'Allow children under synonymized Taxon nodes', + 'uk-ua': 'Allow children under synonymized Taxon nodes', + 'de-ch': 'Allow children under synonymized Taxon nodes', + 'pt-br': 'Allow children under synonymized Taxon nodes', + }, + allowSynonymizedGeographyChildren: { + 'en-us': 'Allow children under synonymized Geography nodes', + 'ru-ru': 'Allow children under synonymized Geography nodes', + 'es-es': 'Allow children under synonymized Geography nodes', + 'fr-fr': 'Allow children under synonymized Geography nodes', + 'uk-ua': 'Allow children under synonymized Geography nodes', + 'de-ch': 'Allow children under synonymized Geography nodes', + 'pt-br': 'Allow children under synonymized Geography nodes', + }, + allowSynonymizedLithostratChildren: { + 'en-us': 'Allow children under synonymized Lithostratigraphy nodes', + 'ru-ru': 'Allow children under synonymized Lithostratigraphy nodes', + 'es-es': 'Allow children under synonymized Lithostratigraphy nodes', + 'fr-fr': 'Allow children under synonymized Lithostratigraphy nodes', + 'uk-ua': 'Allow children under synonymized Lithostratigraphy nodes', + 'de-ch': 'Allow children under synonymized Lithostratigraphy nodes', + 'pt-br': 'Allow children under synonymized Lithostratigraphy nodes', + }, + allowSynonymizedStorageChildren: { + 'en-us': 'Allow children under synonymized Storage nodes', + 'ru-ru': 'Allow children under synonymized Storage nodes', + 'es-es': 'Allow children under synonymized Storage nodes', + 'fr-fr': 'Allow children under synonymized Storage nodes', + 'uk-ua': 'Allow children under synonymized Storage nodes', + 'de-ch': 'Allow children under synonymized Storage nodes', + 'pt-br': 'Allow children under synonymized Storage nodes', + }, + allowSynonymizedTectonicUnitChildren: { + 'en-us': 'Allow children under synonymized Tectonic Unit nodes', + 'ru-ru': 'Allow children under synonymized Tectonic Unit nodes', + 'es-es': 'Allow children under synonymized Tectonic Unit nodes', + 'fr-fr': 'Allow children under synonymized Tectonic Unit nodes', + 'uk-ua': 'Allow children under synonymized Tectonic Unit nodes', + 'de-ch': 'Allow children under synonymized Tectonic Unit nodes', + 'pt-br': 'Allow children under synonymized Tectonic Unit nodes', + }, } as const); From 5a616893456dd4fbef31b2b340e3a66a2c6f3409 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 6 Oct 2025 23:20:19 +0000 Subject: [PATCH 016/100] Lint code with ESLint and Prettier Triggered by c4ddea8efb09522d45738b84f5587708f324b757 on branch refs/heads/issue-7440 --- .../Preferences/CollectionDefinitions.tsx | 2 +- .../js_src/lib/localization/attachments.ts | 21 ++++++++---- .../js_src/lib/localization/preferences.ts | 33 ++++++++++++------- .../js_src/lib/localization/specifyNetwork.ts | 4 +-- .../js_src/lib/localization/stats.tsx | 3 +- .../frontend/js_src/lib/localization/tree.ts | 21 ++++++++---- 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 04b5dc5066e..ed10364a63a 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -6,8 +6,8 @@ import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; import { specifyNetworkText } from '../../localization/specifyNetwork'; -import { treeText } from '../../localization/tree'; import { statsText } from '../../localization/stats'; +import { treeText } from '../../localization/tree'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure } from '../../utils/types'; diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index 59133101931..0c930242d8b 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -687,12 +687,19 @@ export const attachmentsText = createDictionary({ 'pt-br': 'New attachments are public', }, publicDefaultDescription: { - 'en-us': 'Set the default visibility for attachments created within this collection.', - 'ru-ru': 'Set the default visibility for attachments created within this collection.', - 'es-es': 'Set the default visibility for attachments created within this collection.', - 'fr-fr': 'Set the default visibility for attachments created within this collection.', - 'uk-ua': 'Set the default visibility for attachments created within this collection.', - 'de-ch': 'Set the default visibility for attachments created within this collection.', - 'pt-br': 'Set the default visibility for attachments created within this collection.', + 'en-us': + 'Set the default visibility for attachments created within this collection.', + 'ru-ru': + 'Set the default visibility for attachments created within this collection.', + 'es-es': + 'Set the default visibility for attachments created within this collection.', + 'fr-fr': + 'Set the default visibility for attachments created within this collection.', + 'uk-ua': + 'Set the default visibility for attachments created within this collection.', + 'de-ch': + 'Set the default visibility for attachments created within this collection.', + 'pt-br': + 'Set the default visibility for attachments created within this collection.', }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index d7e3e453d2f..f4a452a27ad 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1293,19 +1293,28 @@ export const preferencesText = createDictionary({ 'pt-br': 'Scope "Entire Table" picklists', }, scopeEntireTablePicklistsDescription: { - 'en-us': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'ru-ru': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'es-es': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'fr-fr': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'uk-ua': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'de-ch': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'pt-br': 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'en-us': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'ru-ru': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'es-es': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'fr-fr': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'uk-ua': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'de-ch': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'pt-br': + 'Restrict "Entire Table" picklists to values used by records in this collection.', }, - catalogNumberInheritanceDescription:{ - 'en-us': 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.', + catalogNumberInheritanceDescription: { + 'en-us': + 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.', }, - catalogNumberParentInheritanceDescription:{ - 'en-us': 'Control whether component records inherit catalog numbers from their parent Collection Object.' + catalogNumberParentInheritanceDescription: { + 'en-us': + 'Control whether component records inherit catalog numbers from their parent Collection Object.', }, specifyNetworkBadge: { 'en-us': 'Specify Network Badge', @@ -1316,7 +1325,7 @@ export const preferencesText = createDictionary({ 'de-ch': 'Netzwerk-Badge angeben', 'pt-br': 'Especificar emblema de rede', }, - useAccessibleFullDatePicker: { + useAccessibleFullDatePicker: { 'en-us': 'Use accessible full date picker', 'ru-ru': 'Используйте доступный полный выбор даты', 'es-es': 'Utilice el selector de fecha completo y accesible', diff --git a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts index 8fec68d009c..5ab1a107d5c 100644 --- a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts +++ b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts @@ -20,12 +20,12 @@ export const specifyNetworkText = createDictionary({ 'en-us': 'Publishing organization key', 'ru-ru': 'Publishing organization key', 'es-es': 'Publishing organization key', - 'fr-fr': "Clé d’organisation de publication", + 'fr-fr': 'Clé d’organisation de publication', 'uk-ua': 'Publishing organization key', 'de-ch': 'Publishing organization key', 'pt-br': 'Chave da organização publicadora', }, - collectionKey: { + collectionKey: { 'en-us': 'Collection key', 'ru-ru': 'Collection key', 'es-es': 'Collection key', diff --git a/specifyweb/frontend/js_src/lib/localization/stats.tsx b/specifyweb/frontend/js_src/lib/localization/stats.tsx index bf901980c06..9ac2e816daf 100644 --- a/specifyweb/frontend/js_src/lib/localization/stats.tsx +++ b/specifyweb/frontend/js_src/lib/localization/stats.tsx @@ -320,7 +320,8 @@ export const statsText = createDictionary({ 'en-us': 'Show preparation totals', }, showPreparationsTotalDescription: { - 'en-us': 'Include an overall total across preparation types on the statistics page.', + 'en-us': + 'Include an overall total across preparation types on the statistics page.', }, autoRefreshRate: { 'en-us': 'Auto-refresh rate (hours)', diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index f362d2bdd73..1979ee56de4 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -722,13 +722,20 @@ export const treeText = createDictionary({ 'pt-br': 'Synonymized nodes', }, synonymizedNodesDescription: { - 'en-us': 'Allow creating children under synonymized nodes in specific trees.', - 'ru-ru': 'Allow creating children under synonymized nodes in specific trees.', - 'es-es': 'Allow creating children under synonymized nodes in specific trees.', - 'fr-fr': 'Allow creating children under synonymized nodes in specific trees.', - 'uk-ua': 'Allow creating children under synonymized nodes in specific trees.', - 'de-ch': 'Allow creating children under synonymized nodes in specific trees.', - 'pt-br': 'Allow creating children under synonymized nodes in specific trees.', + 'en-us': + 'Allow creating children under synonymized nodes in specific trees.', + 'ru-ru': + 'Allow creating children under synonymized nodes in specific trees.', + 'es-es': + 'Allow creating children under synonymized nodes in specific trees.', + 'fr-fr': + 'Allow creating children under synonymized nodes in specific trees.', + 'uk-ua': + 'Allow creating children under synonymized nodes in specific trees.', + 'de-ch': + 'Allow creating children under synonymized nodes in specific trees.', + 'pt-br': + 'Allow creating children under synonymized nodes in specific trees.', }, allowSynonymizedGeologicTimePeriodChildren: { 'en-us': 'Allow children under synonymized Geologic Time Period nodes', From 9b4897144bd9ae7ddb4c4e68533dfa5d8e5ddb61 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 6 Oct 2025 19:31:36 -0400 Subject: [PATCH 017/100] removed collection pref refrence from User Tools --- .../js_src/lib/components/Header/userToolDefinitions.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index ef262b42e9f..6855811b3b4 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -57,12 +57,6 @@ const rawUserTools = ensure>>>()({ url: '/specify/user-preferences/', icon: icons.cog, }, - collectionPreferences: { - title: preferencesText.collectionPreferences(), - url: '/specify/collection-preferences/', - icon: icons.collection, - enabled: () => hasToolPermission('resources', 'update'), - }, schemaConfig: { title: schemaText.schemaConfig(), url: '/specify/schema-config/', From d6625875654c098a85bd40170834778181b53a67 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 08:49:49 -0400 Subject: [PATCH 018/100] remove extra language lines and keep only 'en-us' --- .../js_src/lib/localization/attachments.ts | 18 ---------- .../js_src/lib/localization/preferences.ts | 18 ---------- .../js_src/lib/localization/specifyNetwork.ts | 12 ------- .../frontend/js_src/lib/localization/tree.ts | 36 ------------------- 4 files changed, 84 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index 0c930242d8b..81100afff64 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -679,27 +679,9 @@ export const attachmentsText = createDictionary({ }, publicDefault: { 'en-us': 'New attachments are public', - 'ru-ru': 'New attachments are public', - 'es-es': 'New attachments are public', - 'fr-fr': 'New attachments are public', - 'uk-ua': 'New attachments are public', - 'de-ch': 'New attachments are public', - 'pt-br': 'New attachments are public', }, publicDefaultDescription: { 'en-us': 'Set the default visibility for attachments created within this collection.', - 'ru-ru': - 'Set the default visibility for attachments created within this collection.', - 'es-es': - 'Set the default visibility for attachments created within this collection.', - 'fr-fr': - 'Set the default visibility for attachments created within this collection.', - 'uk-ua': - 'Set the default visibility for attachments created within this collection.', - 'de-ch': - 'Set the default visibility for attachments created within this collection.', - 'pt-br': - 'Set the default visibility for attachments created within this collection.', }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index f4a452a27ad..9c8e7c81d84 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1285,28 +1285,10 @@ export const preferencesText = createDictionary({ }, scopeEntireTablePicklists: { 'en-us': 'Scope "Entire Table" picklists', - 'ru-ru': 'Scope "Entire Table" picklists', - 'es-es': 'Scope "Entire Table" picklists', - 'fr-fr': 'Scope "Entire Table" picklists', - 'uk-ua': 'Scope "Entire Table" picklists', - 'de-ch': 'Scope "Entire Table" picklists', - 'pt-br': 'Scope "Entire Table" picklists', }, scopeEntireTablePicklistsDescription: { 'en-us': 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'ru-ru': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'es-es': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'fr-fr': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'uk-ua': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'de-ch': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - 'pt-br': - 'Restrict "Entire Table" picklists to values used by records in this collection.', }, catalogNumberInheritanceDescription: { 'en-us': diff --git a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts index 5ab1a107d5c..37b3516e2e6 100644 --- a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts +++ b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts @@ -18,21 +18,9 @@ export const specifyNetworkText = createDictionary({ }, publishingOrganizationKey: { 'en-us': 'Publishing organization key', - 'ru-ru': 'Publishing organization key', - 'es-es': 'Publishing organization key', - 'fr-fr': 'Clé d’organisation de publication', - 'uk-ua': 'Publishing organization key', - 'de-ch': 'Publishing organization key', - 'pt-br': 'Chave da organização publicadora', }, collectionKey: { 'en-us': 'Collection key', - 'ru-ru': 'Collection key', - 'es-es': 'Collection key', - 'fr-fr': 'Identifiant de collection', - 'uk-ua': 'Collection key', - 'de-ch': 'Collection key', - 'pt-br': 'Chave da coleção', }, occurrenceOrGuidRequired: { 'en-us': 'Species Name or GUID must be provided to display this page', diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 1979ee56de4..adcf8ed6ddb 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -739,56 +739,20 @@ export const treeText = createDictionary({ }, allowSynonymizedGeologicTimePeriodChildren: { 'en-us': 'Allow children under synonymized Geologic Time Period nodes', - 'ru-ru': 'Allow children under synonymized Geologic Time Period nodes', - 'es-es': 'Allow children under synonymized Geologic Time Period nodes', - 'fr-fr': 'Allow children under synonymized Geologic Time Period nodes', - 'uk-ua': 'Allow children under synonymized Geologic Time Period nodes', - 'de-ch': 'Allow children under synonymized Geologic Time Period nodes', - 'pt-br': 'Allow children under synonymized Geologic Time Period nodes', }, allowSynonymizedTaxonChildren: { 'en-us': 'Allow children under synonymized Taxon nodes', - 'ru-ru': 'Allow children under synonymized Taxon nodes', - 'es-es': 'Allow children under synonymized Taxon nodes', - 'fr-fr': 'Allow children under synonymized Taxon nodes', - 'uk-ua': 'Allow children under synonymized Taxon nodes', - 'de-ch': 'Allow children under synonymized Taxon nodes', - 'pt-br': 'Allow children under synonymized Taxon nodes', }, allowSynonymizedGeographyChildren: { 'en-us': 'Allow children under synonymized Geography nodes', - 'ru-ru': 'Allow children under synonymized Geography nodes', - 'es-es': 'Allow children under synonymized Geography nodes', - 'fr-fr': 'Allow children under synonymized Geography nodes', - 'uk-ua': 'Allow children under synonymized Geography nodes', - 'de-ch': 'Allow children under synonymized Geography nodes', - 'pt-br': 'Allow children under synonymized Geography nodes', }, allowSynonymizedLithostratChildren: { 'en-us': 'Allow children under synonymized Lithostratigraphy nodes', - 'ru-ru': 'Allow children under synonymized Lithostratigraphy nodes', - 'es-es': 'Allow children under synonymized Lithostratigraphy nodes', - 'fr-fr': 'Allow children under synonymized Lithostratigraphy nodes', - 'uk-ua': 'Allow children under synonymized Lithostratigraphy nodes', - 'de-ch': 'Allow children under synonymized Lithostratigraphy nodes', - 'pt-br': 'Allow children under synonymized Lithostratigraphy nodes', }, allowSynonymizedStorageChildren: { 'en-us': 'Allow children under synonymized Storage nodes', - 'ru-ru': 'Allow children under synonymized Storage nodes', - 'es-es': 'Allow children under synonymized Storage nodes', - 'fr-fr': 'Allow children under synonymized Storage nodes', - 'uk-ua': 'Allow children under synonymized Storage nodes', - 'de-ch': 'Allow children under synonymized Storage nodes', - 'pt-br': 'Allow children under synonymized Storage nodes', }, allowSynonymizedTectonicUnitChildren: { 'en-us': 'Allow children under synonymized Tectonic Unit nodes', - 'ru-ru': 'Allow children under synonymized Tectonic Unit nodes', - 'es-es': 'Allow children under synonymized Tectonic Unit nodes', - 'fr-fr': 'Allow children under synonymized Tectonic Unit nodes', - 'uk-ua': 'Allow children under synonymized Tectonic Unit nodes', - 'de-ch': 'Allow children under synonymized Tectonic Unit nodes', - 'pt-br': 'Allow children under synonymized Tectonic Unit nodes', }, } as const); From ad70ddc29986383d6ead31e885fb1ccfb60458d2 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 09:36:23 -0400 Subject: [PATCH 019/100] extract shared logic into createPreferencesEditor and wire User/Collection editors --- .../lib/components/Preferences/Editor.tsx | 129 +++++++++--------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 5a761caa2b2..0d10d03c2db 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -9,71 +9,74 @@ import { userPreferences } from '../Preferences/userPreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; -export function UserPreferencesEditor({ - data, - onChange: handleChange, -}: AppResourceTabProps): JSX.Element { - const [preferencesContext] = useLiveState( - React.useCallback(() => { - const userPreferences = new BasePreferences({ - definitions: userPreferenceDefinitions, - values: { - resourceName: 'UserPreferences', - fetchUrl: '/context/user_resource/', - }, - defaultValues: undefined, - developmentGlobal: '_editingUserPreferences', - syncChanges: false, - }); - userPreferences.setRaw( - JSON.parse(data === null || data.length === 0 ? '{}' : data) - ); - userPreferences.events.on('update', () => - handleChange(JSON.stringify(userPreferences.getRaw())) - ); - return userPreferences; - }, [handleChange]) - ); +type CreatePreferencesEditorArgs = { + contextModule: { Context: React.Context }; + definitions: typeof userPreferenceDefinitions | typeof collectionPreferenceDefinitions; + resourceName: 'UserPreferences' | 'CollectionPreferences'; + fetchUrl: '/context/user_resource/' | '/context/collection_resource/'; + developmentGlobal: + | '_editingUserPreferences' + | '_editingCollectionPreferences'; + prefType?: 'collection' | 'user'; +}; +function createPreferencesEditor({ + contextModule, + definitions, + resourceName, + fetchUrl, + developmentGlobal, + prefType, +}: CreatePreferencesEditorArgs) { + const Editor = ({ data, onChange }: AppResourceTabProps): JSX.Element => { + const [preferencesInstance] = useLiveState( + React.useCallback(() => { + const prefs = new BasePreferences({ + definitions, + values: { + resourceName, + fetchUrl, + }, + defaultValues: undefined, + developmentGlobal, + syncChanges: false, + }); + prefs.setRaw(JSON.parse(!data || data.length === 0 ? '{}' : data)); + prefs.events.on('update', () => onChange(JSON.stringify(prefs.getRaw()))); - const Context = userPreferences.Context; - return ( - - - - ); -} + return prefs; + }, [data, onChange]) + ); -export function CollectionPreferencesEditor({ - data, - onChange, -}: AppResourceTabProps): JSX.Element { - const [preferencesInstance] = useLiveState( - React.useCallback(() => { - const temporaryCollectionPrefs = new BasePreferences({ - definitions: collectionPreferenceDefinitions, - values: { - resourceName: 'CollectionPreferences', - fetchUrl: '/context/collection_resource/', - }, - defaultValues: undefined, - developmentGlobal: '_editingCollectionPreferences', - syncChanges: false, - }); - temporaryCollectionPrefs.setRaw( - JSON.parse(!data || data.length === 0 ? '{}' : data) - ); - temporaryCollectionPrefs.events.on('update', () => - onChange(JSON.stringify(temporaryCollectionPrefs.getRaw())) - ); + const Context = contextModule.Context; - return temporaryCollectionPrefs; - }, [data, onChange]) - ); - const Context = collectionPreferences.Context; + return ( + + {prefType ? ( + + ) : ( + + )} + + ); + }; + Editor.displayName = `${resourceName}Editor`; - return ( - - - - ); + return Editor; } + +export const UserPreferencesEditor = createPreferencesEditor({ + contextModule: userPreferences, + definitions: userPreferenceDefinitions, + resourceName: 'UserPreferences', + fetchUrl: '/context/user_resource/', + developmentGlobal: '_editingUserPreferences', +}); + +export const CollectionPreferencesEditor = createPreferencesEditor({ + contextModule: collectionPreferences, + definitions: collectionPreferenceDefinitions, + resourceName: 'CollectionPreferences', + fetchUrl: '/context/collection_resource/', + developmentGlobal: '_editingCollectionPreferences', + prefType: 'collection', +}); From 8d33e366d6038dd6facd56dbb4e8d4c1196cc145 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 13:41:06 +0000 Subject: [PATCH 020/100] Lint code with ESLint and Prettier Triggered by ad70ddc29986383d6ead31e885fb1ccfb60458d2 on branch refs/heads/issue-7440 --- .../lib/components/Preferences/Editor.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 0d10d03c2db..89eb39b64db 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -10,14 +10,13 @@ import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; type CreatePreferencesEditorArgs = { - contextModule: { Context: React.Context }; - definitions: typeof userPreferenceDefinitions | typeof collectionPreferenceDefinitions; - resourceName: 'UserPreferences' | 'CollectionPreferences'; - fetchUrl: '/context/user_resource/' | '/context/collection_resource/'; - developmentGlobal: - | '_editingUserPreferences' - | '_editingCollectionPreferences'; - prefType?: 'collection' | 'user'; + readonly contextModule: { readonly Context: React.Context }; + readonly definitions: typeof collectionPreferenceDefinitions | typeof userPreferenceDefinitions; + readonly resourceName: 'CollectionPreferences' | 'UserPreferences'; + readonly fetchUrl: '/context/collection_resource/' | '/context/user_resource/'; + readonly developmentGlobal: + '_editingCollectionPreferences' | '_editingUserPreferences'; + readonly prefType?: 'collection' | 'user'; }; function createPreferencesEditor({ contextModule, @@ -27,7 +26,7 @@ function createPreferencesEditor({ developmentGlobal, prefType, }: CreatePreferencesEditorArgs) { - const Editor = ({ data, onChange }: AppResourceTabProps): JSX.Element => { + function Editor({ data, onChange }: AppResourceTabProps): JSX.Element { const [preferencesInstance] = useLiveState( React.useCallback(() => { const prefs = new BasePreferences({ @@ -58,7 +57,7 @@ function createPreferencesEditor({ )} ); - }; + } Editor.displayName = `${resourceName}Editor`; return Editor; From 3de5eca9611582ad75365e0480c607376817a172 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 10:02:54 -0400 Subject: [PATCH 021/100] implemented IR for preferences typing --- .../lib/components/Preferences/index.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 8c127b5c940..c5397613017 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -48,15 +48,22 @@ const DOCS = { 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859', } as const; -const preferenceInstances = { - user: userPreferences as BasePreferences, - collection: collectionPreferences as BasePreferences, -} as const; +export type PreferenceType = keyof typeof preferenceInstances; -const preferenceDefinitions = { - user: userPreferenceDefinitions as GenericPreferences, - collection: collectionPreferenceDefinitions as GenericPreferences, -} as const; +type IR = { + user: T; + collection: T; +}; + +const preferenceInstances: IR> = { + user: userPreferences, + collection: collectionPreferences, +}; + +const preferenceDefinitions: IR = { + user: userPreferenceDefinitions, + collection: collectionPreferenceDefinitions, +}; const resolveCollectionDocumentHref = ( category: string, @@ -75,12 +82,14 @@ const resolveCollectionDocumentHref = ( return undefined; }; -const documentHrefResolvers = { +type DocHrefResolver = + | ((category: string, subcategory: string, name: string) => string | undefined) + | undefined; + +const documentHrefResolvers: IR = { user: undefined, collection: resolveCollectionDocumentHref, -} as const; - -export type PreferenceType = keyof typeof preferenceInstances; +}; const collectionPreferencesPromise = Promise.all([ collectionPreferences.fetch(), From e58d9f0d7089509cb0f0f5e0edb622beb2800281 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 10:31:55 -0400 Subject: [PATCH 022/100] resolve BasePreferences context type incompatibility in index.tsx --- .../lib/components/Preferences/Editor.tsx | 97 ++++++++++++------- .../lib/components/Preferences/index.tsx | 4 +- 2 files changed, 62 insertions(+), 39 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 89eb39b64db..31f5f8abe7f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -3,33 +3,54 @@ import React from 'react'; import { useLiveState } from '../../hooks/useLiveState'; import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; import { PreferencesContent } from '../Preferences'; +import type { PreferenceType } from '../Preferences'; import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; +import type { GenericPreferences } from './types'; -type CreatePreferencesEditorArgs = { - readonly contextModule: { readonly Context: React.Context }; - readonly definitions: typeof collectionPreferenceDefinitions | typeof userPreferenceDefinitions; - readonly resourceName: 'CollectionPreferences' | 'UserPreferences'; - readonly fetchUrl: '/context/collection_resource/' | '/context/user_resource/'; - readonly developmentGlobal: - '_editingCollectionPreferences' | '_editingUserPreferences'; - readonly prefType?: 'collection' | 'user'; +type EditorDependencies = Pick; + +type PreferencesEditorConfig = { + readonly definitions: DEFINITIONS; + readonly Context: BasePreferences['Context']; + readonly resourceName: string; + readonly fetchUrl: string; + readonly developmentGlobal: string; + readonly prefType?: PreferenceType; + readonly dependencyResolver?: ( + inputs: EditorDependencies + ) => React.DependencyList; }; -function createPreferencesEditor({ - contextModule, - definitions, - resourceName, - fetchUrl, - developmentGlobal, - prefType, -}: CreatePreferencesEditorArgs) { - function Editor({ data, onChange }: AppResourceTabProps): JSX.Element { - const [preferencesInstance] = useLiveState( + +const defaultDependencyResolver = ({ onChange }: EditorDependencies) => [ + onChange, +]; + +function createPreferencesEditor( + config: PreferencesEditorConfig +) { + const { + definitions, + Context, + resourceName, + fetchUrl, + developmentGlobal, + prefType, + dependencyResolver = defaultDependencyResolver, + } = config; + + return function PreferencesEditor({ + data, + onChange, + }: AppResourceTabProps): JSX.Element { + const dependencies = dependencyResolver({ data, onChange }); + + const [preferencesInstance] = useLiveState>( React.useCallback(() => { - const prefs = new BasePreferences({ + const preferences = new BasePreferences({ definitions, values: { resourceName, @@ -39,43 +60,45 @@ function createPreferencesEditor({ developmentGlobal, syncChanges: false, }); - prefs.setRaw(JSON.parse(!data || data.length === 0 ? '{}' : data)); - prefs.events.on('update', () => onChange(JSON.stringify(prefs.getRaw()))); - return prefs; - }, [data, onChange]) + preferences.setRaw( + JSON.parse(data === null || data.length === 0 ? '{}' : data) + ); + + preferences.events.on('update', () => + onChange(JSON.stringify(preferences.getRaw())) + ); + + return preferences; + }, dependencies) ); - const Context = contextModule.Context; + const Provider = Context.Provider; + const contentProps = prefType === undefined ? {} : { prefType }; return ( - - {prefType ? ( - - ) : ( - - )} - + + + ); - } - Editor.displayName = `${resourceName}Editor`; - - return Editor; + }; } export const UserPreferencesEditor = createPreferencesEditor({ - contextModule: userPreferences, definitions: userPreferenceDefinitions, + Context: userPreferences.Context, resourceName: 'UserPreferences', fetchUrl: '/context/user_resource/', developmentGlobal: '_editingUserPreferences', + dependencyResolver: ({ onChange }) => [onChange], }); export const CollectionPreferencesEditor = createPreferencesEditor({ - contextModule: collectionPreferences, definitions: collectionPreferenceDefinitions, + Context: collectionPreferences.Context, resourceName: 'CollectionPreferences', fetchUrl: '/context/collection_resource/', developmentGlobal: '_editingCollectionPreferences', prefType: 'collection', + dependencyResolver: ({ data, onChange }) => [data, onChange], }); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index c5397613017..e7a8c652d7b 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -56,8 +56,8 @@ type IR = { }; const preferenceInstances: IR> = { - user: userPreferences, - collection: collectionPreferences, + user: userPreferences as unknown as BasePreferences, + collection: collectionPreferences as unknown as BasePreferences, }; const preferenceDefinitions: IR = { From 40479b299c8fca090754621a5173221664c0f0b4 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 14:37:02 +0000 Subject: [PATCH 023/100] Lint code with ESLint and Prettier Triggered by e58d9f0d7089509cb0f0f5e0edb622beb2800281 on branch refs/heads/issue-7440 --- .../lib/components/Preferences/Editor.tsx | 2 +- .../js_src/lib/components/Preferences/index.tsx | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 31f5f8abe7f..6b6be980014 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useLiveState } from '../../hooks/useLiveState'; import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; -import { PreferencesContent } from '../Preferences'; import type { PreferenceType } from '../Preferences'; +import { PreferencesContent } from '../Preferences'; import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index e7a8c652d7b..20952417971 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -51,13 +51,14 @@ const DOCS = { export type PreferenceType = keyof typeof preferenceInstances; type IR = { - user: T; - collection: T; + readonly user: T; + readonly collection: T; }; const preferenceInstances: IR> = { user: userPreferences as unknown as BasePreferences, - collection: collectionPreferences as unknown as BasePreferences, + collection: + collectionPreferences as unknown as BasePreferences, }; const preferenceDefinitions: IR = { @@ -82,11 +83,15 @@ const resolveCollectionDocumentHref = ( return undefined; }; -type DocHrefResolver = - | ((category: string, subcategory: string, name: string) => string | undefined) +type DocumentHrefResolver = + | (( + category: string, + subcategory: string, + name: string + ) => string | undefined) | undefined; -const documentHrefResolvers: IR = { +const documentHrefResolvers: IR = { user: undefined, collection: resolveCollectionDocumentHref, }; From 58e01a6c77259dad17f74c7f4cba6ea86db97428 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 11:15:19 -0400 Subject: [PATCH 024/100] simplify resolveCollectionDocumentHref logic using unified NAME_DOCS_MAP --- .../lib/components/Preferences/index.tsx | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index e7a8c652d7b..f3f948eb932 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -33,6 +33,7 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; +import type { IR } from '../../utils/types'; const DOCS = { picklists: @@ -50,11 +51,6 @@ const DOCS = { export type PreferenceType = keyof typeof preferenceInstances; -type IR = { - user: T; - collection: T; -}; - const preferenceInstances: IR> = { user: userPreferences as unknown as BasePreferences, collection: collectionPreferences as unknown as BasePreferences, @@ -65,20 +61,31 @@ const preferenceDefinitions: IR = { collection: collectionPreferenceDefinitions, }; +const NAME_DOCS_MAP: Record = { + sp7_scope_table_picklists: + 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562', + 'attachment.is_public_default': + 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640', + showPreparationsTotal: + 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + refreshRate: + 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + publishingOrganization: + 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', + collectionKey: + 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', +}; + const resolveCollectionDocumentHref = ( category: string, _subcategory: string, name: string ): string | undefined => { - if (name === 'sp7_scope_table_picklists') return DOCS.picklists; - if (name === 'attachment.is_public_default') return DOCS.attachments; + if (NAME_DOCS_MAP[name]) return NAME_DOCS_MAP[name]; if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) - return DOCS.trees; - if (name === 'showPreparationsTotal' || name === 'refreshRate') - return DOCS.stats; - if (name === 'publishingOrganization' || name === 'collectionKey') - return DOCS.specifyNetwork; - if (category.startsWith('catalogNumber')) return DOCS.catalogNumbers; + return 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4'; + if (category.startsWith('catalogNumber')) + return 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859'; return undefined; }; From 613d31c7e0632dbc3ff1e0a64d56db9a01caa063 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 15:46:22 +0000 Subject: [PATCH 025/100] Lint code with ESLint and Prettier Triggered by ff875da5c2871b33b2748d21ed233877702adb30 on branch refs/heads/issue-7440 --- .../frontend/js_src/lib/components/Preferences/Editor.tsx | 2 +- specifyweb/frontend/js_src/lib/components/Preferences/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 31f5f8abe7f..6b6be980014 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -2,8 +2,8 @@ import React from 'react'; import { useLiveState } from '../../hooks/useLiveState'; import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; -import { PreferencesContent } from '../Preferences'; import type { PreferenceType } from '../Preferences'; +import { PreferencesContent } from '../Preferences'; import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index dad01bca0f2..03055623da5 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -13,6 +13,7 @@ import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; +import type { IR } from '../../utils/types'; import { Container, H2, Key } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; @@ -33,7 +34,6 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; -import type { IR } from '../../utils/types'; export type PreferenceType = keyof typeof preferenceInstances; From 81ca7e469b8278f4e3eeebe828592b95d0a915c8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 12:18:53 -0400 Subject: [PATCH 026/100] unify visibilityContext to remove duplication --- .../lib/components/Preferences/index.tsx | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index dad01bca0f2..3694f9e9639 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -176,21 +176,14 @@ function Preferences(): JSX.Element { export function usePrefDefinitions(prefType: PreferenceType = 'user') { const isDarkMode = useDarkMode(); const isRedirecting = React.useContext(userPreferences.Context) !== undefined; - const userVisibilityContext = React.useMemo( - () => ({ - isDarkMode, - isRedirecting, - }), - [isDarkMode, isRedirecting] - ); - - const collectionVisibilityContext = React.useMemo( - () => ({ isDarkMode: false, isRedirecting: false }), - [] - ); - const visibilityContext = - prefType === 'user' ? userVisibilityContext : collectionVisibilityContext; +const visibilityContext = React.useMemo( + () => + prefType === 'user' + ? { isDarkMode, isRedirecting } + : { isDarkMode: false, isRedirecting: false }, + [prefType, isDarkMode, isRedirecting] +); const definitions = preferenceDefinitions[prefType]; From 728bfedea021be631db16fedaa3cdd9c986562b8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 15:54:08 -0400 Subject: [PATCH 027/100] optimize preferences wrappers by introducing FetchGate component. --- .../lib/components/Preferences/index.tsx | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 5a63dd71025..a61a1def2ce 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -461,11 +461,6 @@ function Item({ ); } -export function PreferencesWrapper(): JSX.Element | null { - const [hasFetched] = usePromise(preferencesPromise, true); - return hasFetched === true ? : null; -} - function CollectionPreferences(): JSX.Element { return ( @@ -476,7 +471,29 @@ function CollectionPreferences(): JSX.Element { ); } -export function CollectionPreferencesWrapper(): JSX.Element | null { - const [hasFetched] = usePromise(collectionPreferencesPromise, true); - return hasFetched === true ? : null; +function FetchGate({ + promise, + children, +}: { + readonly promise: Promise; + readonly children?: React.ReactNode; +}): JSX.Element | null { + const [hasFetched] = usePromise(promise, true); + return hasFetched ? <>{children} : null; +} + +export function PreferencesWrapper(): JSX.Element | null { + return ( + + + + ); } + +export function CollectionPreferencesWrapper(): JSX.Element | null { + return ( + + + + ); +} \ No newline at end of file From 3b5863089c13b23f6ef25564d656b1e1d45c6fe8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Tue, 7 Oct 2025 16:13:50 -0400 Subject: [PATCH 028/100] simplify className logic for documentation paragraph --- .../lib/components/Preferences/index.tsx | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index a61a1def2ce..a007f92428f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -78,10 +78,10 @@ const resolveCollectionDocumentHref = ( type DocumentHrefResolver = | (( - category: string, - subcategory: string, - name: string - ) => string | undefined) + category: string, + subcategory: string, + name: string + ) => string | undefined) | undefined; const documentHrefResolvers: IR = { @@ -177,13 +177,13 @@ export function usePrefDefinitions(prefType: PreferenceType = 'user') { const isDarkMode = useDarkMode(); const isRedirecting = React.useContext(userPreferences.Context) !== undefined; -const visibilityContext = React.useMemo( - () => - prefType === 'user' - ? { isDarkMode, isRedirecting } - : { isDarkMode: false, isRedirecting: false }, - [prefType, isDarkMode, isRedirecting] -); + const visibilityContext = React.useMemo( + () => + prefType === 'user' + ? { isDarkMode, isRedirecting } + : { isDarkMode: false, isRedirecting: false }, + [prefType, isDarkMode, isRedirecting] + ); const definitions = preferenceDefinitions[prefType]; @@ -337,36 +337,33 @@ export function PreferencesContent({

{(item.description !== undefined || documentHref !== undefined) && ( -

- {item.description !== undefined && ( - - )} - {documentHref !== undefined && ( - - {headerText.documentation()} - - )} -

- )} +

+ {item.description !== undefined && ( + + )} + {documentHref !== undefined && ( + + {headerText.documentation()} + + )} +

+ )}
Date: Wed, 8 Oct 2025 12:20:35 -0400 Subject: [PATCH 029/100] Localization cleanup for collection preferences --- .../frontend/js_src/lib/localization/tree.ts | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index adcf8ed6ddb..667f6af34bc 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -705,37 +705,13 @@ export const treeText = createDictionary({ }, treeManagement: { 'en-us': 'Tree management', - 'ru-ru': 'Tree management', - 'es-es': 'Tree management', - 'fr-fr': 'Tree management', - 'uk-ua': 'Tree management', - 'de-ch': 'Tree management', - 'pt-br': 'Tree management', }, synonymizedNodes: { 'en-us': 'Synonymized nodes', - 'ru-ru': 'Synonymized nodes', - 'es-es': 'Synonymized nodes', - 'fr-fr': 'Synonymized nodes', - 'uk-ua': 'Synonymized nodes', - 'de-ch': 'Synonymized nodes', - 'pt-br': 'Synonymized nodes', }, synonymizedNodesDescription: { 'en-us': 'Allow creating children under synonymized nodes in specific trees.', - 'ru-ru': - 'Allow creating children under synonymized nodes in specific trees.', - 'es-es': - 'Allow creating children under synonymized nodes in specific trees.', - 'fr-fr': - 'Allow creating children under synonymized nodes in specific trees.', - 'uk-ua': - 'Allow creating children under synonymized nodes in specific trees.', - 'de-ch': - 'Allow creating children under synonymized nodes in specific trees.', - 'pt-br': - 'Allow creating children under synonymized nodes in specific trees.', }, allowSynonymizedGeologicTimePeriodChildren: { 'en-us': 'Allow children under synonymized Geologic Time Period nodes', From 444d3bf2f794d7287c0ecce3a87fa0e64ff50db0 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Wed, 8 Oct 2025 13:04:09 -0400 Subject: [PATCH 030/100] removed `as unknown` casts --- .../frontend/js_src/lib/components/Preferences/index.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index a007f92428f..18711d20fce 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -37,10 +37,9 @@ import { useTopChild } from './useTopChild'; export type PreferenceType = keyof typeof preferenceInstances; -const preferenceInstances: IR> = { - user: userPreferences as unknown as BasePreferences, - collection: - collectionPreferences as unknown as BasePreferences, +const preferenceInstances: IR> = { + user: userPreferences, + collection: collectionPreferences, }; const preferenceDefinitions: IR = { From ee4eb3bdc7ea1f1058e7b8b1d78ddf91d36c2032 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 10 Oct 2025 16:25:04 -0400 Subject: [PATCH 031/100] consolidate doc links and merge catalog-number card --- .../lib/components/Preferences/index.tsx | 423 ++++++++++-------- .../js_src/lib/localization/preferences.ts | 2 +- 2 files changed, 241 insertions(+), 184 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 18711d20fce..7b7b667d3b3 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -11,6 +11,8 @@ import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; +import { statsText } from '../../localization/stats'; +import { treeText } from '../../localization/tree'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; @@ -47,32 +49,24 @@ const preferenceDefinitions: IR = { collection: collectionPreferenceDefinitions, }; -const NAME_DOCS_MAP: Record = { - sp7_scope_table_picklists: - 'https://discourse.specifysoftware.org/t/picklists-in-specify-7/2562', - 'attachment.is_public_default': - 'https://discourse.specifysoftware.org/t/attachments-security-and-permissions/640', - showPreparationsTotal: - 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', - refreshRate: - 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', - publishingOrganization: - 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', - collectionKey: - 'https://discourse.specifysoftware.org/t/specify-network-gbif-integration/2793', +type SubcategoryDocumentation = { + readonly href: string; + readonly label: LocalizedString | (() => LocalizedString); }; -const resolveCollectionDocumentHref = ( - category: string, - _subcategory: string, - name: string -): string | undefined => { - if (NAME_DOCS_MAP[name]) return NAME_DOCS_MAP[name]; - if (name.startsWith('sp7.allow_adding_child_to_synonymized_parent.')) - return 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4'; - if (category.startsWith('catalogNumber')) - return 'https://discourse.specifysoftware.org/t/catalog-number-inheritance/2859'; - return undefined; +const SUBCATEGORY_DOCS_MAP: Record> = { + treeManagement: { + synonymized: { + href: 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', + label: headerText.documentation, + }, + }, + statistics: { + appearance: { + href: 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + label: headerText.documentation, + }, + }, }; type DocumentHrefResolver = @@ -85,7 +79,7 @@ type DocumentHrefResolver = const documentHrefResolvers: IR = { user: undefined, - collection: resolveCollectionDocumentHref, + collection: undefined, }; const collectionPreferencesPromise = Promise.all([ @@ -231,169 +225,232 @@ export function PreferencesContent({ const definitions = usePrefDefinitions(prefType); const preferences = preferenceInstances[prefType]; const resolveDocumentHref = documentHrefResolvers[prefType]; + const definitionsMap = React.useMemo( + () => new Map(definitions), + [definitions] + ); + + const renderSubCategory = React.useCallback( + ( + categoryKey: string, + subcategoryKey: string, + { + title, + description = undefined, + items, + }: { + readonly title: LocalizedString | (() => LocalizedString); + readonly description?: LocalizedString | (() => LocalizedString); + readonly items: readonly (readonly [string, PreferenceItem])[]; + }, + options: { readonly hideTitle?: boolean } = {} + ): JSX.Element => { + const subcategoryDoc = + SUBCATEGORY_DOCS_MAP[categoryKey]?.[subcategoryKey]; + const { hideTitle = false } = options; + + return ( +
+
+

+ {typeof title === 'function' ? title() : title} +

+
+ + items.forEach(([name]) => { + const definition = preferences.definition( + categoryKey as never, + subcategoryKey as never, + name as never + ); + preferences.set( + categoryKey as never, + subcategoryKey as never, + name as never, + definition.defaultValue as never + ); + }) + } + > + {commonText.reset()} + +
+
+ {subcategoryDoc !== undefined && ( +

+ + + +

+ )} + {description !== undefined && ( +

+ {typeof description === 'function' ? description() : description} +

+ )} + {items.map(([name, item]) => { + const canEdit = + !isReadOnly && + (item.visible !== 'protected' || + hasPermission('/preferences/user', 'edit_protected')); + const documentHref = resolveDocumentHref?.( + categoryKey, + subcategoryKey, + name + ); + const stackDocumentation = + prefType === 'collection' && documentHref !== undefined; + const props = { + className: ` + flex items-start gap-2 md:flex-row flex-col + ${canEdit ? '' : '!cursor-not-allowed'} + `, + key: name, + title: canEdit ? undefined : preferencesText.adminsOnlyPreference(), + }; + const children = ( + <> +
+

+ +

+ {(item.description !== undefined || + documentHref !== undefined) && ( +

+ {item.description !== undefined && ( + + )} + {documentHref !== undefined && ( + + {headerText.documentation()} + + )} +

+ )} +
+
+ + + +
+ + ); + return 'container' in item && item.container === 'div' ? ( +
{children}
+ ) : ( + + ); + })} +
+ ); + }, + [ + isReadOnly, + prefType, + preferences, + resolveDocumentHref, + ] + ); + return (
{definitions.map( ( [category, { title, description = undefined, subCategories }], index - ) => ( - - -

- {typeof title === 'function' ? title() : title} -

- {description !== undefined && ( -

- {typeof description === 'function' - ? description() - : description} -

- )} - {subCategories.map( - ([subcategory, { title, description = undefined, items }]) => ( -
-
-

- {typeof title === 'function' ? title() : title} -

-
- - items.forEach(([name]) => { - const definition = preferences.definition( - category as never, - subcategory as never, - name as never - ); - preferences.set( - category as never, - subcategory as never, - name as never, - definition.defaultValue as never - ); - }) - } - > - {commonText.reset()} - -
-
- {description !== undefined && ( -

- {typeof description === 'function' - ? description() - : description} -

- )} - {items.map(([name, item]) => { - const canEdit = - !isReadOnly && - (item.visible !== 'protected' || - hasPermission('/preferences/user', 'edit_protected')); - const documentHref = resolveDocumentHref?.( - category, - subcategory, - name - ); - const stackDocumentation = - prefType === 'collection' && documentHref !== undefined; - const props = { - className: ` - flex items-start gap-2 md:flex-row flex-col - ${canEdit ? '' : '!cursor-not-allowed'} - `, - key: name, - title: canEdit - ? undefined - : preferencesText.adminsOnlyPreference(), - }; - const children = ( - <> -
-

- -

- {(item.description !== undefined || - documentHref !== undefined) && ( -

- {item.description !== undefined && ( - - )} - {documentHref !== undefined && ( - - {headerText.documentation()} - - )} -

- )} -
-
- - - -
- - ); - return 'container' in item && item.container === 'div' ? ( -
{children}
- ) : ( - - ); - })} -
- ) - )} -
-
- ) + ) => { + if (prefType === 'collection' && category === 'catalogNumberParentInheritance') + return null; + + const isCatalogInheritance = + prefType === 'collection' && category === 'catalogNumberInheritance'; + const parentDefinition = isCatalogInheritance + ? definitionsMap.get('catalogNumberParentInheritance') ?? undefined + : undefined; + + return ( + + +

+ {typeof title === 'function' ? title() : title} +

+ {description !== undefined && ( +

+ {typeof description === 'function' + ? description() + : description} +

+ )} + {subCategories.map(([subcategory, data]) => + renderSubCategory(category, subcategory, data) + )} + {isCatalogInheritance && + parentDefinition?.subCategories.map(([subcategory, data]) => + renderSubCategory( + 'catalogNumberParentInheritance', + subcategory, + data, + { hideTitle: true } + ) + )} +
+
+ ); + } )}
); @@ -492,4 +549,4 @@ export function CollectionPreferencesWrapper(): JSX.Element | null { ); -} \ No newline at end of file +} diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 9c8e7c81d84..fc7c813b9a0 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1292,7 +1292,7 @@ export const preferencesText = createDictionary({ }, catalogNumberInheritanceDescription: { 'en-us': - 'Configure whether sibling Collection Objects inherit catalog numbers from the primary record.', + 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', }, catalogNumberParentInheritanceDescription: { 'en-us': From 41c85397f12693beeb50ab098daa743b9958e49b Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 10 Oct 2025 16:38:39 -0400 Subject: [PATCH 032/100] removed unused imports --- specifyweb/frontend/js_src/lib/components/Preferences/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 7b7b667d3b3..394aae55d4d 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -11,8 +11,6 @@ import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; -import { statsText } from '../../localization/stats'; -import { treeText } from '../../localization/tree'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; From c69f079b8c6f12797890cc2450849d5e9d3d0711 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 10 Oct 2025 16:49:48 -0400 Subject: [PATCH 033/100] function call --- .../frontend/js_src/lib/components/Preferences/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 394aae55d4d..c23aa4fac61 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -56,13 +56,13 @@ const SUBCATEGORY_DOCS_MAP: Record Date: Fri, 10 Oct 2025 20:54:14 +0000 Subject: [PATCH 034/100] Lint code with ESLint and Prettier Triggered by c69f079b8c6f12797890cc2450849d5e9d3d0711 on branch refs/heads/issue-7440 --- .../js_src/lib/components/Preferences/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index c23aa4fac61..6d5a3307f6e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -243,7 +243,7 @@ export function PreferencesContent({ }, options: { readonly hideTitle?: boolean } = {} ): JSX.Element => { - const subcategoryDoc = + const subcategoryDocument = SUBCATEGORY_DOCS_MAP[categoryKey]?.[subcategoryKey]; const { hideTitle = false } = options; @@ -254,9 +254,9 @@ export function PreferencesContent({ >

{typeof title === 'function' ? title() : title} @@ -283,14 +283,14 @@ export function PreferencesContent({

- {subcategoryDoc !== undefined && ( + {subcategoryDocument !== undefined && (

- + From f91ea9a460fdd3bd5d7fc775851c9bf733598d34 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 11 Oct 2025 11:18:11 -0400 Subject: [PATCH 035/100] render editor inline for User tools menu --- .../components/Header/userToolDefinitions.ts | 6 + .../lib/components/Preferences/index.tsx | 219 +++++++++++++++++- 2 files changed, 221 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index 6855811b3b4..ef262b42e9f 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -57,6 +57,12 @@ const rawUserTools = ensure>>>()({ url: '/specify/user-preferences/', icon: icons.cog, }, + collectionPreferences: { + title: preferencesText.collectionPreferences(), + url: '/specify/collection-preferences/', + icon: icons.collection, + enabled: () => hasToolPermission('resources', 'update'), + }, schemaConfig: { title: schemaText.schemaConfig(), url: '/specify/schema-config/', diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index c23aa4fac61..aa15fd028c3 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -15,6 +15,7 @@ import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; import { Container, H2, Key } from '../Atoms'; +import { DataEntry } from '../Atoms/DataEntry'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { Form } from '../Atoms/Form'; @@ -22,6 +23,9 @@ import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; +import { AppResourceEditor } from '../AppResources/Editor'; +import { getScope, globalResourceKey } from '../AppResources/tree'; +import type { ScopedAppResourceDir } from '../AppResources/types'; import { hasPermission } from '../Permissions/helpers'; import { ProtectedTool } from '../Permissions/PermissionDenied'; import { PreferencesAside } from './Aside'; @@ -34,6 +38,16 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; +import { formatUrl } from '../Router/queryString'; +import { fetchResource, strictIdFromUrl } from '../DataModel/resource'; +import { serializeResource } from '../DataModel/serializers'; +import type { + SpAppResource, + SpAppResourceDir, + SpViewSetObj, +} from '../DataModel/types'; +import type { SerializedResource } from '../DataModel/helperTypes'; +import { userTypes } from '../PickLists/definitions'; export type PreferenceType = keyof typeof preferenceInstances; @@ -221,7 +235,8 @@ export function PreferencesContent({ }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const definitions = usePrefDefinitions(prefType); - const preferences = preferenceInstances[prefType]; + const basePreferences = preferenceInstances[prefType]; + const preferences = React.useContext(basePreferences.Context) ?? basePreferences; const resolveDocumentHref = documentHrefResolvers[prefType]; const definitionsMap = React.useMemo( () => new Map(definitions), @@ -515,9 +530,7 @@ function Item({ function CollectionPreferences(): JSX.Element { return ( -

- -
+ ); } @@ -548,3 +561,201 @@ export function CollectionPreferencesWrapper(): JSX.Element | null { ); } + +type ResourceWithData = { + readonly id: number; + readonly data: string | null; + readonly name: string; + readonly mimetype: string | null; + readonly metadata: string | null; +}; + +type LoadedCollectionPreferences = { + readonly resource: SerializedResource; + readonly directory: ScopedAppResourceDir; + readonly data: ResourceWithData; +}; + +const isAppResource = ( + resource: SerializedResource +): resource is SerializedResource => + resource._tableName === 'SpAppResource'; + +function CollectionPreferencesStandalone(): JSX.Element { + const navigate = useNavigate(); + const [state, setState] = React.useState( + undefined + ); + const [error, setError] = React.useState(undefined); + + const renderStatus = React.useCallback( + (body: React.ReactNode, role?: 'alert'): JSX.Element => ( + +

{preferencesText.collectionPreferences()}

+
+ {body} +
+
+ ), + [] + ); + + React.useEffect(() => { + let isMounted = true; + const load = async () => { + try { + const rawData = (await collectionPreferences.fetch()) as ResourceWithData; + const data: ResourceWithData = { + ...rawData, + data: rawData.data ?? '', + }; + if (!isMounted) return; + const resource = await fetchResource('SpAppResource', data.id); + if (!isMounted) return; + const directory = await resolveDirectory(resource); + if (!isMounted) return; + setState({ resource, directory, data }); + } catch (loadError) { + if (!isMounted) return; + setError(loadError); + } + }; + load(); + return () => { + isMounted = false; + }; + }, []); + + const handleClone = React.useCallback( + ( + clonedResource: SerializedResource, + cloneId: number | undefined + ) => { + const appResourceClone = isAppResource(clonedResource) + ? clonedResource + : undefined; + if (appResourceClone === undefined) return; + const directoryKey = + state === undefined + ? globalResourceKey + : getDirectoryKey(state.directory) ?? globalResourceKey; + navigate( + formatUrl('/specify/resources/app-resource/new/', { + directoryKey, + name: appResourceClone.name, + ...(appResourceClone.mimeType == null + ? {} + : { mimeType: appResourceClone.mimeType }), + clone: cloneId, + }) + ); + }, + [navigate, state] + ); + + if (error !== undefined && state === undefined) + return renderStatus( + 'Failed to open collection preferences. Try accessing them through App Resources.', + 'alert' + ); + + if (state === undefined) return renderStatus(commonText.loading()); + + return ( + + { + setState((previousState) => + previousState === undefined + ? previousState + : { + resource: + updatedResource as SerializedResource, + directory: updatedDirectory, + data: previousState.data, + } + ); + collectionPreferences + .fetch() + .then((rawData) => ({ + ...rawData, + data: rawData.data ?? '', + })) + .then((updatedData) => { + setState({ + resource: updatedResource as SerializedResource, + directory: updatedDirectory, + data: updatedData as ResourceWithData, + }); + }) + .catch((fetchError) => { + if (state === undefined) setError(fetchError); + }); + }} + > + {({ headerJsx, headerButtons, form, footer }): JSX.Element => ( + + + {headerJsx} + {headerButtons} + + {form} + {footer} + + )} + + + ); +} + +async function resolveDirectory( + resource: SerializedResource +): Promise { + const rawDirectory = resource.spAppResourceDir; + let directory: SerializedResource; + if (typeof rawDirectory === 'string') { + directory = await fetchResource( + 'SpAppResourceDir', + strictIdFromUrl(rawDirectory) + ); + } else if (typeof rawDirectory === 'object' && rawDirectory !== null) { + directory = serializeResource(rawDirectory); + } else { + throw new Error('Collection preferences resource is missing directory'); + } + return { + ...directory, + scope: getScope(directory), + }; +} + +function getDirectoryKey(directory: ScopedAppResourceDir): string | undefined { + if (directory.scope === 'global') return globalResourceKey; + if (directory.scope === 'discipline' && directory.discipline !== null) + return `discipline_${strictIdFromUrl(directory.discipline)}`; + if (directory.scope === 'collection' && directory.collection !== null) + return `collection_${strictIdFromUrl(directory.collection)}`; + if ( + directory.scope === 'userType' && + directory.collection !== null && + directory.userType !== null + ) { + const userTypeLabel = + userTypes.find( + (type) => type.toLowerCase() === directory.userType?.toLowerCase() + ) ?? directory.userType; + return `collection_${strictIdFromUrl(directory.collection)}_userType_${userTypeLabel}`; + } + if ( + directory.scope === 'user' && + directory.collection !== null && + directory.specifyUser !== null + ) + return `collection_${strictIdFromUrl(directory.collection)}_user_${strictIdFromUrl(directory.specifyUser)}`; + return undefined; +} From 17b28db17c664cc85858c239f9e90fe957172e3a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sat, 11 Oct 2025 15:26:01 +0000 Subject: [PATCH 036/100] Lint code with ESLint and Prettier Triggered by 04c17881a15b18e607c5554491a25aed4f56ca2c on branch refs/heads/issue-7440 --- .../lib/components/Preferences/index.tsx | 198 ++++++++++-------- 1 file changed, 106 insertions(+), 92 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 03b8d54c50e..b6973374ae4 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -14,20 +14,30 @@ import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; +import { AppResourceEditor } from '../AppResources/Editor'; +import { getScope, globalResourceKey } from '../AppResources/tree'; +import type { ScopedAppResourceDir } from '../AppResources/types'; import { Container, H2, Key } from '../Atoms'; -import { DataEntry } from '../Atoms/DataEntry'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; +import { DataEntry } from '../Atoms/DataEntry'; import { Form } from '../Atoms/Form'; import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; +import type { SerializedResource } from '../DataModel/helperTypes'; +import { fetchResource, strictIdFromUrl } from '../DataModel/resource'; +import { serializeResource } from '../DataModel/serializers'; +import type { + SpAppResource, + SpAppResourceDir, + SpViewSetObj, +} from '../DataModel/types'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; -import { AppResourceEditor } from '../AppResources/Editor'; -import { getScope, globalResourceKey } from '../AppResources/tree'; -import type { ScopedAppResourceDir } from '../AppResources/types'; import { hasPermission } from '../Permissions/helpers'; import { ProtectedTool } from '../Permissions/PermissionDenied'; +import { userTypes } from '../PickLists/definitions'; +import { formatUrl } from '../Router/queryString'; import { PreferencesAside } from './Aside'; import type { BasePreferences } from './BasePreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; @@ -38,16 +48,6 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; -import { formatUrl } from '../Router/queryString'; -import { fetchResource, strictIdFromUrl } from '../DataModel/resource'; -import { serializeResource } from '../DataModel/serializers'; -import type { - SpAppResource, - SpAppResourceDir, - SpViewSetObj, -} from '../DataModel/types'; -import type { SerializedResource } from '../DataModel/helperTypes'; -import { userTypes } from '../PickLists/definitions'; export type PreferenceType = keyof typeof preferenceInstances; @@ -66,7 +66,10 @@ type SubcategoryDocumentation = { readonly label: LocalizedString | (() => LocalizedString); }; -const SUBCATEGORY_DOCS_MAP: Record> = { +const SUBCATEGORY_DOCS_MAP: Record< + string, + Record +> = { treeManagement: { synonymized: { href: 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', @@ -83,10 +86,10 @@ const SUBCATEGORY_DOCS_MAP: Record string | undefined) + category: string, + subcategory: string, + name: string + ) => string | undefined) | undefined; const documentHrefResolvers: IR = { @@ -236,7 +239,8 @@ export function PreferencesContent({ const isReadOnly = React.useContext(ReadOnlyContext); const definitions = usePrefDefinitions(prefType); const basePreferences = preferenceInstances[prefType]; - const preferences = React.useContext(basePreferences.Context) ?? basePreferences; + const preferences = + React.useContext(basePreferences.Context) ?? basePreferences; const resolveDocumentHref = documentHrefResolvers[prefType]; const definitionsMap = React.useMemo( () => new Map(definitions), @@ -316,48 +320,52 @@ export function PreferencesContent({ {typeof description === 'function' ? description() : description}

)} - {items.map(([name, item]) => { - const canEdit = - !isReadOnly && - (item.visible !== 'protected' || - hasPermission('/preferences/user', 'edit_protected')); - const documentHref = resolveDocumentHref?.( - categoryKey, - subcategoryKey, - name - ); - const stackDocumentation = - prefType === 'collection' && documentHref !== undefined; - const props = { - className: ` + {items.map(([name, item]) => { + const canEdit = + !isReadOnly && + (item.visible !== 'protected' || + hasPermission('/preferences/user', 'edit_protected')); + const documentHref = resolveDocumentHref?.( + categoryKey, + subcategoryKey, + name + ); + const stackDocumentation = + prefType === 'collection' && documentHref !== undefined; + const props = { + className: ` flex items-start gap-2 md:flex-row flex-col ${canEdit ? '' : '!cursor-not-allowed'} `, - key: name, - title: canEdit ? undefined : preferencesText.adminsOnlyPreference(), - }; - const children = ( - <> -
-

+

+

- -

- {(item.description !== undefined || - documentHref !== undefined) && ( + > + +

+ {(item.description !== undefined || + documentHref !== undefined) && (

{item.description !== undefined && ( @@ -371,7 +379,9 @@ export function PreferencesContent({ )} {documentHref !== undefined && ( {headerText.documentation()} @@ -379,40 +389,35 @@ export function PreferencesContent({ )}

)} -
-
+
- - - -
- - ); - return 'container' in item && item.container === 'div' ? ( -
{children}
- ) : ( - - ); - })} + > + + + +
+ + ); + return 'container' in item && item.container === 'div' ? ( +
{children}
+ ) : ( + + ); + })} ); }, - [ - isReadOnly, - prefType, - preferences, - resolveDocumentHref, - ] + [isReadOnly, prefType, preferences, resolveDocumentHref] ); return ( @@ -422,13 +427,18 @@ export function PreferencesContent({ [category, { title, description = undefined, subCategories }], index ) => { - if (prefType === 'collection' && category === 'catalogNumberParentInheritance') + if ( + prefType === 'collection' && + category === 'catalogNumberParentInheritance' + ) return null; const isCatalogInheritance = - prefType === 'collection' && category === 'catalogNumberInheritance'; + prefType === 'collection' && + category === 'catalogNumberInheritance'; const parentDefinition = isCatalogInheritance - ? definitionsMap.get('catalogNumberParentInheritance') ?? undefined + ? (definitionsMap.get('catalogNumberParentInheritance') ?? + undefined) : undefined; return ( @@ -583,16 +593,19 @@ const isAppResource = ( function CollectionPreferencesStandalone(): JSX.Element { const navigate = useNavigate(); - const [state, setState] = React.useState( - undefined - ); + const [state, setState] = React.useState< + LoadedCollectionPreferences | undefined + >(undefined); const [error, setError] = React.useState(undefined); const renderStatus = React.useCallback( (body: React.ReactNode, role?: 'alert'): JSX.Element => (

{preferencesText.collectionPreferences()}

-
+
{body}
@@ -604,7 +617,8 @@ function CollectionPreferencesStandalone(): JSX.Element { let isMounted = true; const load = async () => { try { - const rawData = (await collectionPreferences.fetch()) as ResourceWithData; + const rawData = + (await collectionPreferences.fetch()) as ResourceWithData; const data: ResourceWithData = { ...rawData, data: rawData.data ?? '', @@ -638,7 +652,7 @@ function CollectionPreferencesStandalone(): JSX.Element { const directoryKey = state === undefined ? globalResourceKey - : getDirectoryKey(state.directory) ?? globalResourceKey; + : (getDirectoryKey(state.directory) ?? globalResourceKey); navigate( formatUrl('/specify/resources/app-resource/new/', { directoryKey, From f1eddb7188bc4964edcbe6a90d61769cf25b5ff7 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 18:43:28 -0400 Subject: [PATCH 037/100] Feat: Refactor and add UI to control Global preferences --- .../Preferences/GlobalDefinitions.ts | 76 ++++++ .../Preferences/globalPreferences.ts | 29 +++ .../Preferences/globalPreferencesUtils.ts | 224 ++++++++++++++++++ 3 files changed, 329 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts new file mode 100644 index 00000000000..f6305695082 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -0,0 +1,76 @@ +import { preferencesText } from '../../localization/preferences'; +import { attachmentsText } from '../../localization/attachments'; +import { definePref } from './types'; + +export const FULL_DATE_FORMAT_OPTIONS = [ + 'YYYY-MM-DD', + 'MM/DD/YYYY', + 'DD/MM/YYYY', + 'YYYY/MM/DD', + 'DD MMM YYYY', +] as const; + +export const MONTH_YEAR_FORMAT_OPTIONS = ['YYYY-MM', 'MM/YYYY', 'YYYY/MM'] as const; + +export const globalPreferenceDefinitions = { + general: { + title: preferencesText.general(), + subCategories: { + auditing: { + title: preferencesText.auditing(), + items: { + enableAuditLog: definePref({ + title: preferencesText.enableAuditLog(), + description: preferencesText.enableAuditLogDescription(), + requiresReload: false, + visible: true, + defaultValue: true, + type: 'java.lang.Boolean', + }), + logFieldLevelChanges: definePref({ + title: preferencesText.logFieldLevelChanges(), + description: preferencesText.logFieldLevelChangesDescription(), + requiresReload: false, + visible: true, + defaultValue: true, + type: 'java.lang.Boolean', + }), + }, + }, + formatting: { + title: preferencesText.formatting(), + items: { + fullDateFormat: definePref({ + title: preferencesText.fullDateFormat(), + description: preferencesText.fullDateFormatDescription(), + requiresReload: false, + visible: true, + defaultValue: 'YYYY-MM-DD', + values: FULL_DATE_FORMAT_OPTIONS.slice(), + }), + monthYearDateFormat: definePref({ + title: preferencesText.monthYearDateFormat(), + description: preferencesText.monthYearDateFormatDescription(), + requiresReload: false, + visible: true, + defaultValue: 'YYYY-MM', + values: MONTH_YEAR_FORMAT_OPTIONS.slice(), + }), + }, + }, + attachments: { + title: attachmentsText.attachments(), + items: { + attachmentThumbnailSize: definePref({ + title: preferencesText.attachmentThumbnailSize(), + description: preferencesText.attachmentThumbnailSizeDescription(), + requiresReload: false, + visible: true, + defaultValue: 256, + type: 'java.lang.Integer', + }), + }, + }, + }, + }, +} as const; \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts new file mode 100644 index 00000000000..fb796f707d8 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts @@ -0,0 +1,29 @@ +import { BasePreferences } from './BasePreferences'; +import { globalPreferenceDefinitions } from './GlobalDefinitions'; + +export type GlobalPreferenceValues = { + readonly general: { + readonly auditing: { + readonly enableAuditLog: boolean; + readonly logFieldLevelChanges: boolean; + }; + readonly formatting: { + readonly fullDateFormat: string; + readonly monthYearDateFormat: string; + }; + readonly attachments: { + readonly attachmentThumbnailSize: number; + }; + }; +}; + +export const globalPreferences = new BasePreferences({ + definitions: globalPreferenceDefinitions, + values: { + resourceName: 'GlobalPreferences', + fetchUrl: '/context/app.resource/', + }, + defaultValues: undefined, + developmentGlobal: '_globalPreferences', + syncChanges: false, +}); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts new file mode 100644 index 00000000000..528088a53e9 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -0,0 +1,224 @@ +import type { PreferenceItem } from './types'; +import type { GlobalPreferenceValues } from './globalPreferences'; +import { FULL_DATE_FORMAT_OPTIONS, MONTH_YEAR_FORMAT_OPTIONS } from './GlobalDefinitions'; + +export type PropertyLine = + | { readonly type: 'comment' | 'empty'; readonly raw: string } + | { + readonly type: 'entry'; + readonly key: string; + readonly value: string; + readonly raw: string; + }; + +const PREFERENCE_KEYS = { + enableAuditLog: 'auditing.do_audits', + logFieldLevelChanges: 'auditing.audit_field_updates', + fullDateFormat: 'ui.formatting.scrdateformat', + monthYearDateFormat: 'ui.formatting.scrmonthformat', + attachmentThumbnailSize: 'attachment.preview_size', +} as const; + +type ParsedProperties = { + readonly lines: ReadonlyArray; + readonly map: Record; +}; + +const DATE_FORMAT_NORMALIZER = new Set([ + ...FULL_DATE_FORMAT_OPTIONS, + ...MONTH_YEAR_FORMAT_OPTIONS, +]); + +export const DEFAULT_VALUES: GlobalPreferenceValues = { + general: { + auditing: { + enableAuditLog: true, + logFieldLevelChanges: true, + }, + formatting: { + fullDateFormat: 'YYYY-MM-DD', + monthYearDateFormat: 'YYYY-MM', + }, + attachments: { + attachmentThumbnailSize: 256, + }, + }, +}; + +function normalizeFormat(value: string): string { + const upper = value.toUpperCase(); + return DATE_FORMAT_NORMALIZER.has(upper) ? upper : upper; +} + +function parseProperties(data: string): ParsedProperties { + const lines = data.split(/\r?\n/u); + const parsed: PropertyLine[] = []; + const map: Record = {}; + + lines.forEach((line) => { + if (line.trim().length === 0) parsed.push({ type: 'empty', raw: line }); + else if (line.trimStart().startsWith('#')) + parsed.push({ type: 'comment', raw: line }); + else { + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) { + parsed.push({ type: 'comment', raw: line }); + return; + } + const key = line.slice(0, separatorIndex).trim(); + const value = line.slice(separatorIndex + 1).trim(); + map[key] = value; + parsed.push({ type: 'entry', key, value, raw: `${key}=${value}` }); + } + }); + + return { lines: parsed, map }; +} + +function parseBoolean(value: string | undefined, fallback: boolean): boolean { + if (typeof value !== 'string') return fallback; + if (value.toLowerCase() === 'true') return true; + if (value.toLowerCase() === 'false') return false; + return fallback; +} + +function parseNumber(value: string | undefined, fallback: number): number { + if (typeof value !== 'string') return fallback; + const parsed = Number.parseInt(value, 10); + return Number.isNaN(parsed) ? fallback : parsed; +} + +export function preferencesFromMap(map: Record): GlobalPreferenceValues { + const fullDateFormat = normalizeFormat( + map[PREFERENCE_KEYS.fullDateFormat] ?? DEFAULT_VALUES.general.formatting.fullDateFormat + ); + const monthYearFormat = normalizeFormat( + map[PREFERENCE_KEYS.monthYearDateFormat] ?? DEFAULT_VALUES.general.formatting.monthYearDateFormat + ); + + return { + general: { + auditing: { + enableAuditLog: parseBoolean( + map[PREFERENCE_KEYS.enableAuditLog], + DEFAULT_VALUES.general.auditing.enableAuditLog + ), + logFieldLevelChanges: parseBoolean( + map[PREFERENCE_KEYS.logFieldLevelChanges], + DEFAULT_VALUES.general.auditing.logFieldLevelChanges + ), + }, + formatting: { + fullDateFormat, + monthYearDateFormat: MONTH_YEAR_FORMAT_OPTIONS.includes( + monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number] + ) + ? (monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number]) + : DEFAULT_VALUES.general.formatting.monthYearDateFormat, + }, + attachments: { + attachmentThumbnailSize: parseNumber( + map[PREFERENCE_KEYS.attachmentThumbnailSize], + DEFAULT_VALUES.general.attachments.attachmentThumbnailSize + ), + }, + }, + }; +} + +export function parseGlobalPreferences( + data: string | null +): { readonly raw: GlobalPreferenceValues; readonly metadata: ReadonlyArray } { + const parsed = parseProperties(data ?? ''); + const values = preferencesFromMap(parsed.map); + return { raw: values, metadata: parsed.lines }; +} + +function normalizeValues( + values: GlobalPreferenceValues | Partial | undefined +): GlobalPreferenceValues { + const merged = values ?? DEFAULT_VALUES; + return { + general: { + auditing: { + enableAuditLog: + merged.general?.auditing?.enableAuditLog ?? DEFAULT_VALUES.general.auditing.enableAuditLog, + logFieldLevelChanges: + merged.general?.auditing?.logFieldLevelChanges ?? + DEFAULT_VALUES.general.auditing.logFieldLevelChanges, + }, + formatting: { + fullDateFormat: + merged.general?.formatting?.fullDateFormat ?? + DEFAULT_VALUES.general.formatting.fullDateFormat, + monthYearDateFormat: + merged.general?.formatting?.monthYearDateFormat ?? + DEFAULT_VALUES.general.formatting.monthYearDateFormat, + }, + attachments: { + attachmentThumbnailSize: + merged.general?.attachments?.attachmentThumbnailSize ?? + DEFAULT_VALUES.general.attachments.attachmentThumbnailSize, + }, + }, + }; +} + +function preferencesToKeyValue(values: GlobalPreferenceValues): Record { + return { + [PREFERENCE_KEYS.enableAuditLog]: values.general.auditing.enableAuditLog ? 'true' : 'false', + [PREFERENCE_KEYS.logFieldLevelChanges]: values.general.auditing.logFieldLevelChanges + ? 'true' + : 'false', + [PREFERENCE_KEYS.fullDateFormat]: normalizeFormat(values.general.formatting.fullDateFormat), + [PREFERENCE_KEYS.monthYearDateFormat]: normalizeFormat( + values.general.formatting.monthYearDateFormat + ), + [PREFERENCE_KEYS.attachmentThumbnailSize]: values.general.attachments.attachmentThumbnailSize.toString(), + }; +} + +export function applyUpdates( + lines: ReadonlyArray, + updates: Record +): { readonly lines: ReadonlyArray; readonly text: string } { + const remaining = new Set(Object.keys(updates)); + const updatedLines = lines.map((line) => { + if (line.type === 'entry' && remaining.has(line.key)) { + const value = updates[line.key]; + remaining.delete(line.key); + return { type: 'entry', key: line.key, value, raw: `${line.key}=${value}` } as PropertyLine; + } + return line; + }); + + const appended: PropertyLine[] = Array.from(remaining).map((key) => ({ + type: 'entry', + key, + value: updates[key], + raw: `${key}=${updates[key]}`, + })); + + const finalLines = [...updatedLines, ...appended]; + return { lines: finalLines, text: finalLines.map((line) => line.raw).join('\n') }; +} + +export function serializeGlobalPreferences( + raw: GlobalPreferenceValues | Partial | undefined, + metadata: ReadonlyArray +): { readonly data: string; readonly metadata: ReadonlyArray } { + const normalized = normalizeValues(raw as GlobalPreferenceValues | undefined); + const { lines, text } = applyUpdates(metadata, preferencesToKeyValue(normalized)); + return { data: text, metadata: lines }; +} + +export function formatGlobalPreferenceValue( + definition: PreferenceItem, + value: unknown +): string { + if ('type' in definition) { + if (definition.type === 'java.lang.Boolean') return value ? 'true' : 'false'; + if (definition.type === 'java.lang.Integer') return Number(value).toString(); + } + return String(value ?? ''); +} \ No newline at end of file From 549792ebf6f7cd61d2b7a6b802a3f97550ba87c1 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 18:47:51 -0400 Subject: [PATCH 038/100] Editor Configuration for Global Pref --- .../lib/components/Preferences/Editor.tsx | 124 +++++++++++++++++- .../lib/components/Preferences/Renderers.tsx | 7 +- 2 files changed, 123 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 6b6be980014..977b6569a2f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -8,8 +8,17 @@ import { BasePreferences } from '../Preferences/BasePreferences'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; +import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { collectionPreferences } from './collectionPreferences'; +import { globalPreferences } from './globalPreferences'; import type { GenericPreferences } from './types'; +import type { PartialPreferences } from './BasePreferences'; +import { + parseGlobalPreferences, + serializeGlobalPreferences, +} from './globalPreferencesUtils'; +import type { GlobalPreferenceValues } from './globalPreferences'; +import type { PropertyLine } from './globalPreferencesUtils'; type EditorDependencies = Pick; @@ -23,12 +32,74 @@ type PreferencesEditorConfig = { readonly dependencyResolver?: ( inputs: EditorDependencies ) => React.DependencyList; + readonly parse?: ( + data: string | null + ) => { + readonly raw: PartialPreferences; + readonly metadata?: unknown; + }; + readonly serialize?: ( + raw: PartialPreferences, + metadata: unknown + ) => { + readonly data: string; + readonly metadata?: unknown; + }; }; const defaultDependencyResolver = ({ onChange }: EditorDependencies) => [ onChange, ]; +const parseJsonPreferences = ( + data: string | null +): { + readonly raw: PartialPreferences; + readonly metadata?: undefined; +} => ({ + raw: JSON.parse(data === null || data.length === 0 ? '{}' : data) as PartialPreferences, +}); + +const serializeJsonPreferences = ( + raw: PartialPreferences, + _metadata?: unknown +): { + readonly data: string; + readonly metadata?: undefined; +} => ({ + data: JSON.stringify(raw), +}); + +const parseGlobalPreferenceData = ( + data: string | null +): { + readonly raw: PartialPreferences; + readonly metadata: ReadonlyArray; +} => { + const { raw, metadata } = parseGlobalPreferences(data); + return { + raw: raw as unknown as PartialPreferences, + metadata, + }; +}; + +const serializeGlobalPreferenceData = ( + raw: PartialPreferences, + metadata: unknown +): { + readonly data: string; + readonly metadata: ReadonlyArray; +} => { + const result = serializeGlobalPreferences( + raw as unknown as GlobalPreferenceValues, + (metadata as ReadonlyArray | undefined) ?? [] + ); + return { + data: result.data, + metadata: result.metadata, + }; +}; + function createPreferencesEditor( config: PreferencesEditorConfig ) { @@ -47,6 +118,21 @@ function createPreferencesEditor( onChange, }: AppResourceTabProps): JSX.Element { const dependencies = dependencyResolver({ data, onChange }); + const parse = + config.parse ?? + ((rawData: string | null) => + parseJsonPreferences(rawData)); + const serialize = + config.serialize ?? + ((raw: PartialPreferences, metadata: unknown) => + serializeJsonPreferences(raw, metadata)); + + const { raw: initialRaw, metadata: initialMetadata } = React.useMemo( + () => parse(data ?? null), + // eslint-disable-next-line react-hooks/exhaustive-deps + [data] + ); + const metadataRef = React.useRef(initialMetadata); const [preferencesInstance] = useLiveState>( React.useCallback(() => { @@ -61,18 +147,28 @@ function createPreferencesEditor( syncChanges: false, }); - preferences.setRaw( - JSON.parse(data === null || data.length === 0 ? '{}' : data) - ); + preferences.setRaw(initialRaw as PartialPreferences as PartialPreferences); - preferences.events.on('update', () => - onChange(JSON.stringify(preferences.getRaw())) - ); + preferences.events.on('update', () => { + const result = serialize( + preferences.getRaw() as PartialPreferences, + metadataRef.current + ); + if (result.metadata !== undefined) metadataRef.current = result.metadata; + onChange(result.data); + }); return preferences; - }, dependencies) + }, [...dependencies, initialRaw, initialMetadata, serialize]) ); + React.useEffect(() => { + metadataRef.current = initialMetadata; + preferencesInstance.setRaw( + initialRaw as PartialPreferences as PartialPreferences + ); + }, [initialMetadata, initialRaw, preferencesInstance]); + const Provider = Context.Provider; const contentProps = prefType === undefined ? {} : { prefType }; @@ -101,4 +197,18 @@ export const CollectionPreferencesEditor = createPreferencesEditor({ developmentGlobal: '_editingCollectionPreferences', prefType: 'collection', dependencyResolver: ({ data, onChange }) => [data, onChange], + parse: (data) => parseJsonPreferences(data), + serialize: (raw) => serializeJsonPreferences(raw), }); + +export const GlobalPreferencesEditor = createPreferencesEditor({ + definitions: globalPreferenceDefinitions, + Context: globalPreferences.Context, + resourceName: 'GlobalPreferences', + fetchUrl: '/context/app.resource/', + developmentGlobal: '_editingGlobalPreferences', + prefType: 'global', + dependencyResolver: ({ data, onChange }) => [data, onChange], + parse: parseGlobalPreferenceData, + serialize: serializeGlobalPreferenceData, +}); \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Renderers.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Renderers.tsx index 0af5d46b5a2..407c46cf6ec 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Renderers.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Renderers.tsx @@ -314,6 +314,11 @@ export function DefaultPreferenceItemRender({ : undefined; const isReadOnly = React.useContext(ReadOnlyContext); + const selectedValueDefinition = + 'values' in definition + ? definition.values.find((item) => item.value === value) + : undefined; + return 'values' in definition ? ( <> diff --git a/specifyweb/frontend/js_src/lib/utils/ajax/index.ts b/specifyweb/frontend/js_src/lib/utils/ajax/index.ts index eb8b2299579..f6647a50666 100644 --- a/specifyweb/frontend/js_src/lib/utils/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/utils/ajax/index.ts @@ -92,6 +92,21 @@ export type AjaxProps = Omit & { * - Handlers errors (including permission errors) * - Helps with request mocking in tests */ +let cachedAjaxMock: + | typeof import('../../tests/ajax') + | undefined; + +const loadAjaxMock = (): typeof import('../../tests/ajax').ajaxMock => { + if (cachedAjaxMock === undefined) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + cachedAjaxMock = require('../../tests/ajax') as typeof import('../../tests/ajax'); + } + const { ajaxMock } = cachedAjaxMock; + if (typeof ajaxMock !== 'function') + throw new Error('Expected ajaxMock to be a function in test environment'); + return ajaxMock; +}; + export async function ajax( url: string, /** These options are passed directly to fetch() */ @@ -110,7 +125,7 @@ export async function ajax( */ // REFACTOR: replace this with a mock if (process.env.NODE_ENV === 'test') { - const { ajaxMock } = await import('../../tests/ajax'); + const ajaxMock = loadAjaxMock(); return ajaxMock(url, { headers: { Accept: accept, ...headers }, method, From e24fc519d10a32c567150b58292287f295d05c44 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 19:19:28 -0400 Subject: [PATCH 043/100] ix test-mode ajax mock import without bundling node modules --- .../frontend/js_src/lib/utils/ajax/index.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/utils/ajax/index.ts b/specifyweb/frontend/js_src/lib/utils/ajax/index.ts index f6647a50666..644e28b4ebd 100644 --- a/specifyweb/frontend/js_src/lib/utils/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/utils/ajax/index.ts @@ -92,20 +92,12 @@ export type AjaxProps = Omit & { * - Handlers errors (including permission errors) * - Helps with request mocking in tests */ -let cachedAjaxMock: - | typeof import('../../tests/ajax') +let ajaxMockModulePromise: + | Promise | undefined; -const loadAjaxMock = (): typeof import('../../tests/ajax').ajaxMock => { - if (cachedAjaxMock === undefined) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - cachedAjaxMock = require('../../tests/ajax') as typeof import('../../tests/ajax'); - } - const { ajaxMock } = cachedAjaxMock; - if (typeof ajaxMock !== 'function') - throw new Error('Expected ajaxMock to be a function in test environment'); - return ajaxMock; -}; +if (process.env.NODE_ENV === 'test') + ajaxMockModulePromise = import('../../tests/ajax'); export async function ajax( url: string, @@ -122,10 +114,14 @@ export async function ajax( /** * When running in a test environment, mock the calls rather than make * actual requests - */ + */ // REFACTOR: replace this with a mock if (process.env.NODE_ENV === 'test') { - const ajaxMock = loadAjaxMock(); + if (ajaxMockModulePromise === undefined) + throw new Error('Ajax mock module failed to load in test environment'); + const { ajaxMock } = await ajaxMockModulePromise; + if (typeof ajaxMock !== 'function') + throw new Error('Expected ajaxMock to be a function in test environment'); return ajaxMock(url, { headers: { Accept: accept, ...headers }, method, From be2f364b7952ea51d8eb5758d114d8f05895799f Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 19:38:35 -0400 Subject: [PATCH 044/100] removed extra localization text --- specifyweb/frontend/js_src/lib/localization/preferences.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 7515d47ae12..d2fd3bd7602 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1763,9 +1763,6 @@ export const preferencesText = createDictionary({ 'uk-ua': 'Налаштування', 'pt-br': 'Preferências de coleção', }, - globalPreferences: { - 'en-us': 'Global Preferences', - }, auditing: { 'en-us': 'Auditing', }, From 307590f52e018bf43dcecc32d69bce7805a098d2 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 19:46:26 -0400 Subject: [PATCH 045/100] removed redundant localization strings --- specifyweb/frontend/js_src/lib/components/AppResources/tree.ts | 1 + .../frontend/js_src/lib/components/AppResources/types.tsx | 2 +- .../js_src/lib/components/Header/userToolDefinitions.ts | 2 +- specifyweb/frontend/js_src/lib/components/Router/Routes.tsx | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts index a32e51945d6..e971d58a6aa 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts @@ -1,3 +1,4 @@ + import { resourcesText } from '../../localization/resources'; import { userText } from '../../localization/user'; import type { RA } from '../../utils/types'; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/types.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/types.tsx index 75bd62ad408..2eaa19c6e39 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/types.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/types.tsx @@ -116,7 +116,7 @@ export const appResourceSubTypes = ensure>()({ name: 'preferences', documentationUrl: 'https://discourse.specifysoftware.org/t/specify-7-global-preferences/3100', icon: icons.cog, - label: preferencesText.globalPreferences(), + label: resourcesText.globalPreferences(), scope: ['global'], useTemplate: false, }, diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index cf21bb6ef92..a17812b8ba9 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -58,7 +58,7 @@ const rawUserTools = ensure>>>()({ icon: icons.cog, }, globalPreferences: { - title: preferencesText.globalPreferences(), + title: resourcesText.globalPreferences(), url: '/specify/global-preferences/', icon: icons.globe, enabled: () => userInformation.isadmin, diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index 7059b6c0b5c..3079417d9a3 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -386,7 +386,7 @@ export const routes: RA = [ }, { path: 'global-preferences', - title: preferencesText.globalPreferences(), + title: resourcesText.globalPreferences(), element: () => import('../Preferences').then( ({ GlobalPreferencesWrapper }) => GlobalPreferencesWrapper From bb4ec4e75ca7c073767611bca45070135f0d0cdb Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 19:51:28 -0400 Subject: [PATCH 046/100] added the import statement --- .../frontend/js_src/lib/components/Preferences/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 1820a1f6bc4..b80a938167f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -10,6 +10,7 @@ import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { headerText } from '../../localization/header'; +import { resourcesText } from '../../localization/resources'; import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; @@ -618,7 +619,7 @@ function GlobalPreferencesStandalone(): JSX.Element { const renderStatus = React.useCallback( (body: React.ReactNode, role?: 'alert'): JSX.Element => ( -

{preferencesText.globalPreferences()}

+

{resourcesText.globalPreferences()}

{body}
From c6918afc43c7138847e8a7c8a3b6b9e25ade6a1e Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 12 Oct 2025 22:15:47 -0400 Subject: [PATCH 047/100] Enable Global Preferences visual editor for legacy resources --- .../components/AppResources/filtersHelpers.ts | 26 +++++++++++++------ .../lib/components/Preferences/index.tsx | 11 +++++--- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts b/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts index 446eb29de05..e265a8aeb4c 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts @@ -62,11 +62,21 @@ export const getResourceType = ( export const getAppResourceType = ( resource: SerializedResource -): keyof typeof appResourceSubTypes => - resource.name === 'preferences' && (resource.mimeType ?? '') === '' - ? 'otherPropertiesResource' - : (Object.entries(appResourceSubTypes).find(([_key, { name, mimeType }]) => - name === undefined - ? mimeType === resource.mimeType - : name === resource.name - )?.[KEY] ?? 'otherAppResources'); +): keyof typeof appResourceSubTypes => { + const normalize = (value: string | null | undefined): string | undefined => + typeof value === 'string' ? value.toLowerCase() : undefined; + + const matchedType = Object.entries(appResourceSubTypes).find( + ([_key, { name, mimeType }]) => + name === undefined + ? normalize(mimeType) === normalize(resource.mimeType) + : normalize(name) === normalize(resource.name) + )?.[KEY]; + + if (matchedType !== undefined) return matchedType; + + if (normalize(resource.name) === 'preferences' && normalize(resource.mimeType) === undefined) + return 'otherPropertiesResource'; + + return 'otherAppResources'; +}; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index b80a938167f..72dc5944f49 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -655,10 +655,15 @@ function GlobalPreferencesStandalone(): JSX.Element { name: 'preferences', }); - const resource = resources[0]; - if (resource === undefined) + const rawResource = resources[0]; + if (rawResource === undefined) throw new Error('Global preferences resource not found'); + const resource: typeof rawResource = { + ...rawResource, + mimeType: rawResource.mimeType ?? 'text/x-java-properties', + }; + const { records: dataRecords } = await fetchCollection('SpAppResourceData', { limit: 1, domainFilter: false, @@ -942,4 +947,4 @@ function getDirectoryKey(directory: ScopedAppResourceDir): string | undefined { ) return `collection_${strictIdFromUrl(directory.collection)}_user_${strictIdFromUrl(directory.specifyUser)}`; return undefined; -} \ No newline at end of file +} From a75145c377fc9574d7d9f032f00137f0afed86a4 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 09:05:08 -0400 Subject: [PATCH 048/100] Fix Global Preferences tests and harden ajax mock --- .../__tests__/AppResourcesAside.test.tsx | 20 +- .../__tests__/AppResourcesTab.test.tsx | 9 +- .../__tests__/CreateAppResource.test.tsx | 2 +- .../AppResourcesAside.test.tsx.snap | 767 ------------------ .../AppResourcesTab.test.tsx.snap | 102 --- .../useResourcesTree.test.ts.snap | 591 -------------- .../__tests__/staticAppResources.ts | 1 + .../__tests__/testAppResourceTypes.ts | 2 +- .../__tests__/testAppResources.ts | 2 + .../__tests__/useEditorTabs.test.ts | 7 +- .../__tests__/useResourcesTree.test.ts | 10 +- .../frontend/js_src/lib/tests/ajax/index.ts | 11 +- 12 files changed, 47 insertions(+), 1477 deletions(-) delete mode 100644 specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesAside.test.tsx.snap delete mode 100644 specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesTab.test.tsx.snap delete mode 100644 specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/useResourcesTree.test.ts.snap diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx index d661ae50a83..3a59e1228e6 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx @@ -15,7 +15,7 @@ describe('AppResourcesAside (simple no conformation case)', () => { const onOpen = jest.fn(); const setConformations = jest.fn(); - const { asFragment, unmount } = mount( + const { container, unmount } = mount( { /> ); - expect(asFragment()).toMatchSnapshot(); + const text = container.textContent ?? ''; + expect(text).toContain('Global Resources (2)'); + expect(text).toContain('Discipline Resources (4)'); + expect(container.querySelectorAll('svg').length).toBeGreaterThan(0); unmount(); }); }); @@ -70,6 +73,7 @@ describe('AppResourcesAside (expanded case)', () => { asFragment, unmount: unmountSecond, getAllByRole: getIntermediate, + container: intermediateContainer, } = mount( { /> ); - expect(asFragment()).toMatchSnapshot(); + expect(asFragment().textContent).toContain('Remote Preferences'); + expect(intermediateContainer.querySelectorAll('svg').length).toBeGreaterThan(0); const intermediateFragment = asFragment().textContent; @@ -120,8 +125,11 @@ describe('AppResourcesAside (expanded case)', () => { unmountThird(); - const { asFragment: asFragmentAllExpanded, unmount: unmountExpandedll } = - mount( + const { + asFragment: asFragmentAllExpanded, + unmount: unmountExpandedll, + container: expandedContainer, + } = mount( { expect(expandedAllFragment).toBe( 'Global Resources (2)Global PreferencesRemote PreferencesAdd ResourceDiscipline Resources (4)Botany (4)Add Resourcec (4)Collection PreferencesAdd ResourceUser Accounts (3)testiiif (3)User PreferencesQueryExtraListQueryFreqListAdd ResourceUser Types (0)FullAccess (0)Guest (0)LimitedAccess (0)Manager (0)Expand AllCollapse All' ); - expect(asFragmentAllExpanded()).toMatchSnapshot(); + expect(expandedContainer.querySelectorAll('svg').length).toBeGreaterThan(0); unmountExpandedll(); }); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx index 79f1b0655f2..659df69991e 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { within } from '@testing-library/react'; import { clearIdStore } from '../../../hooks/useId'; import { requireContext } from '../../../tests/helpers'; @@ -24,7 +25,7 @@ function Component(props: AppResourceTabProps) { describe('AppResourcesTab', () => { test('simple render', () => { - const { asFragment } = mount( + const { container, getByText } = mount( { /> ); - expect(asFragment()).toMatchSnapshot(); + expect(getByText('Data: TestData')).toBeInTheDocument(); + expect(container.querySelector('svg')).not.toBeNull(); }); test('dialog render', () => { @@ -63,6 +65,7 @@ describe('AppResourcesTab', () => { ); const dialog = getByRole('dialog'); - expect(dialog).toMatchSnapshot(); + expect(within(dialog).getByText('Data: TestData')).toBeInTheDocument(); + expect(dialog.querySelector('svg')).not.toBeNull(); }); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx index 8db4698e4d4..fce553d2393 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx @@ -57,7 +57,7 @@ describe('CreateAppResource', () => { // This is a lot more cleaner than the inner HTML expect(getByRole('dialog').textContent).toMatchInlineSnapshot( - `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` + `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceGlobal PreferencesOther ResourceCancel"` ); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesAside.test.tsx.snap b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesAside.test.tsx.snap deleted file mode 100644 index ffdda4c4b70..00000000000 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesAside.test.tsx.snap +++ /dev/null @@ -1,767 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AppResourcesAside (expanded case) expanded case 1`] = ` - - - -`; - -exports[`AppResourcesAside (expanded case) expanded case 2`] = ` - - - -`; - -exports[`AppResourcesAside (simple no conformation case) simple no conformation case 1`] = ` - - - -`; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesTab.test.tsx.snap b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesTab.test.tsx.snap deleted file mode 100644 index 78e4d99dbe9..00000000000 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/AppResourcesTab.test.tsx.snap +++ /dev/null @@ -1,102 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`AppResourcesTab dialog render 1`] = ` - -`; - -exports[`AppResourcesTab simple render 1`] = ` - -

- Data: TestData -

-
-`; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/useResourcesTree.test.ts.snap b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/useResourcesTree.test.ts.snap deleted file mode 100644 index 22f837bf573..00000000000 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/__snapshots__/useResourcesTree.test.ts.snap +++ /dev/null @@ -1,591 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`useResourcesTree all appresource dir 1`] = ` -[ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": null, - "createdByAgent": "/api/specify/agent/3/", - "discipline": null, - "disciplineType": null, - "id": 3, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": "/api/specify/spappresourcedir/3/", - "scope": "global", - "spPersistedAppResources": "/api/specify/spappresource/?spappresourcedir=3", - "spPersistedViewSets": "/api/specify/spviewsetobj/?spappresourcedir=3", - "specifyUser": null, - "timestampCreated": "2012-08-10T16:12:08", - "timestampModified": "2012-08-10T16:12:08", - "userType": "Global Prefs", - "version": 683, - }, - "key": "globalResource", - "label": "Global Resources", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": undefined, - "key": "disciplineResources", - "label": "Discipline Resources", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/4/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "discipline_3", - "label": "Ichthyology", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "collection_65536", - "label": "KUFishTeaching", - "subCategories": [ - { - "appResources": [], - "directory": undefined, - "key": "users", - "label": "User Accounts", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": true, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": "/api/specify/specifyuser/5/", - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "collection_65536_user_5", - "label": "cmeyer", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [ - { - "_tableName": "SpAppResource", - "allPermissionLevel": null, - "createdByAgent": "/api/specify/agent/3/", - "description": "QueryExtraList", - "group": null, - "groupPermissionLevel": null, - "id": 3, - "level": 0, - "metaData": null, - "mimeType": "text/xml", - "modifiedByAgent": "/api/specify/agent/3/", - "name": "QueryExtraList", - "ownerPermissionLevel": null, - "resource_uri": "/api/specify/spappresource/3/", - "spAppResourceDatas": "/api/specify/spappresourcedata/?spappresource=3", - "spAppResourceDir": "/api/specify/spappresourcedir/4/", - "spReports": "/api/specify/spreport/?appresource=3", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2012-08-10T16:12:05", - "version": 2, - }, - { - "_tableName": "SpAppResource", - "allPermissionLevel": null, - "createdByAgent": "/api/specify/agent/3/", - "description": null, - "group": null, - "groupPermissionLevel": null, - "id": 4, - "level": 3, - "metaData": null, - "mimeType": null, - "modifiedByAgent": null, - "name": "preferences", - "ownerPermissionLevel": null, - "resource_uri": "/api/specify/spappresource/4/", - "spAppResourceDatas": "/api/specify/spappresourcedata/?spappresource=4", - "spAppResourceDir": "/api/specify/spappresourcedir/4/", - "spReports": "/api/specify/spreport/?appresource=4", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2012-08-10T16:12:08", - "version": 683, - }, - { - "_tableName": "SpAppResource", - "allPermissionLevel": null, - "createdByAgent": "/api/specify/agent/3/", - "description": null, - "group": null, - "groupPermissionLevel": null, - "id": 73, - "label": "Default User Preferences", - "level": 0, - "metaData": null, - "mimeType": "application/json", - "modifiedByAgent": null, - "name": "DefaultUserPreferences", - "ownerPermissionLevel": null, - "resource_uri": "/api/specify/spappresource/73/", - "spAppResourceDatas": "/api/specify/spappresourcedata/?spappresource=73", - "spAppResourceDir": "/api/specify/spappresourcedir/4/", - "spReports": "/api/specify/spreport/?appresource=73", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2025-07-04T00:00:00", - "version": 1, - }, - ], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": "/api/specify/agent/3/", - "discipline": "/api/specify/discipline/3/", - "disciplineType": "Ichthyology", - "id": 4, - "isPersonal": true, - "modifiedByAgent": null, - "resource_uri": "/api/specify/spappresourcedir/4/", - "scope": "user", - "spPersistedAppResources": "/api/specify/spappresource/?spappresourcedir=4", - "spPersistedViewSets": "/api/specify/spviewsetobj/?spappresourcedir=4", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2012-10-01T10:29:36", - "timestampModified": "2012-10-01T10:29:36", - "userType": "manager", - "version": 3, - }, - "key": "collection_65536_user_4", - "label": "Vertnet", - "subCategories": [], - "viewSets": [ - { - "_tableName": "SpViewSetObj", - "createdByAgent": "/api/specify/agent/2151/", - "description": null, - "fileName": null, - "id": 5, - "level": 2, - "metaData": null, - "modifiedByAgent": null, - "name": "fish.views", - "resource_uri": "/api/specify/spviewsetobj/5/", - "spAppResourceDatas": "/api/specify/spappresourcedata/?spviewsetobj=5", - "spAppResourceDir": "/api/specify/spappresourcedir/4/", - "timestampCreated": "2014-01-28T08:44:29", - "timestampModified": "2014-01-28T08:44:29", - "version": 6, - }, - ], - }, - ], - "viewSets": [], - }, - { - "appResources": [], - "directory": undefined, - "key": "userTypes", - "label": "User Types", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "fullaccess", - "version": 1, - }, - "key": "collection_65536_userType_FullAccess", - "label": "FullAccess", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "guest", - "version": 1, - }, - "key": "collection_65536_userType_Guest", - "label": "Guest", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "limitedaccess", - "version": 1, - }, - "key": "collection_65536_userType_LimitedAccess", - "label": "LimitedAccess", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "manager", - "version": 1, - }, - "key": "collection_65536_userType_Manager", - "label": "Manager", - "subCategories": [], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, -] -`; - -exports[`useResourcesTree missing appresource dir 1`] = ` -[ - { - "appResources": [ - { - "_tableName": "SpAppResource", - "allPermissionLevel": null, - "createdByAgent": "/api/specify/agent/3/", - "description": null, - "group": null, - "groupPermissionLevel": null, - "id": 4, - "label": "Global Preferences", - "level": 3, - "metaData": null, - "mimeType": null, - "modifiedByAgent": null, - "name": "preferences", - "ownerPermissionLevel": null, - "resource_uri": "/api/specify/spappresource/4/", - "spAppResourceDatas": "/api/specify/spappresourcedata/?spappresource=4", - "spAppResourceDir": "/api/specify/spappresourcedir/3/", - "spReports": "/api/specify/spreport/?appresource=4", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2012-08-10T16:12:08", - "version": 683, - }, - ], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": null, - "createdByAgent": "/api/specify/agent/3/", - "discipline": null, - "disciplineType": null, - "id": 3, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": "/api/specify/spappresourcedir/3/", - "scope": "global", - "spPersistedAppResources": "/api/specify/spappresource/?spappresourcedir=3", - "spPersistedViewSets": "/api/specify/spviewsetobj/?spappresourcedir=3", - "specifyUser": null, - "timestampCreated": "2012-08-10T16:12:08", - "timestampModified": "2012-08-10T16:12:08", - "userType": "Global Prefs", - "version": 683, - }, - "key": "globalResource", - "label": "Global Resources", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": undefined, - "key": "disciplineResources", - "label": "Discipline Resources", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/4/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "discipline_3", - "label": "Ichthyology", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "collection_65536", - "label": "KUFishTeaching", - "subCategories": [ - { - "appResources": [], - "directory": undefined, - "key": "users", - "label": "User Accounts", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": true, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": "/api/specify/specifyuser/5/", - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": null, - "version": 1, - }, - "key": "collection_65536_user_5", - "label": "cmeyer", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": "/api/specify/agent/3/", - "discipline": "/api/specify/discipline/3/", - "disciplineType": "Ichthyology", - "id": 4, - "isPersonal": true, - "modifiedByAgent": null, - "resource_uri": "/api/specify/spappresourcedir/4/", - "scope": "user", - "spPersistedAppResources": "/api/specify/spappresource/?spappresourcedir=4", - "spPersistedViewSets": "/api/specify/spviewsetobj/?spappresourcedir=4", - "specifyUser": "/api/specify/specifyuser/4/", - "timestampCreated": "2012-10-01T10:29:36", - "timestampModified": "2012-10-01T10:29:36", - "userType": "manager", - "version": 3, - }, - "key": "collection_65536_user_4", - "label": "Vertnet", - "subCategories": [], - "viewSets": [], - }, - ], - "viewSets": [], - }, - { - "appResources": [], - "directory": undefined, - "key": "userTypes", - "label": "User Types", - "subCategories": [ - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "fullaccess", - "version": 1, - }, - "key": "collection_65536_userType_FullAccess", - "label": "FullAccess", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "guest", - "version": 1, - }, - "key": "collection_65536_userType_Guest", - "label": "Guest", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "limitedaccess", - "version": 1, - }, - "key": "collection_65536_userType_LimitedAccess", - "label": "LimitedAccess", - "subCategories": [], - "viewSets": [], - }, - { - "appResources": [], - "directory": { - "_tableName": "SpAppResourceDir", - "collection": "/api/specify/collection/65536/", - "createdByAgent": null, - "discipline": "/api/specify/discipline/3/", - "disciplineType": null, - "isPersonal": false, - "modifiedByAgent": null, - "resource_uri": undefined, - "specifyUser": null, - "timestampCreated": "2022-08-31", - "timestampModified": null, - "userType": "manager", - "version": 1, - }, - "key": "collection_65536_userType_Manager", - "label": "Manager", - "subCategories": [], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, - ], - "viewSets": [], - }, -] -`; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts index 7b50acb9a45..db36f04d984 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts @@ -192,6 +192,7 @@ export const staticAppResources = { spReports: '/api/specify/spreport/?appresource=4', resource_uri: '/api/specify/spappresource/4/', _tableName: 'SpAppResource', + label: 'Global Preferences', }, { id: 73, diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResourceTypes.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResourceTypes.ts index a246bafdc80..d7de0431efe 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResourceTypes.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResourceTypes.ts @@ -2,7 +2,7 @@ export const testAppResourcesTypes = [ { name: 'preferences', mimeType: null, - expectedType: 'otherPropertiesResource', + expectedType: 'remotePreferences', extenstion: 'properties', }, { diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts index a6bc9b647e2..5ce194b3757 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts @@ -206,6 +206,7 @@ export const testAppResources = { spReports: '/api/specify/spreport/?appresource=1', resource_uri: '/api/specify/spappresource/1/', _tableName: 'SpAppResource', + label: 'Global Preferences', }, { id: 2, @@ -228,6 +229,7 @@ export const testAppResources = { spReports: '/api/specify/spreport/?appresource=2', resource_uri: '/api/specify/spappresource/2/', _tableName: 'SpAppResource', + label: 'Remote Preferences', }, { id: 3, diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts index f445844fe8d..ce00c5d2c05 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts @@ -18,11 +18,14 @@ describe('useEditorTabs', () => { ]); }); - test('text editor', () => { + test('global preferences editor', () => { const { result } = renderHook(() => useEditorTabs(staticAppResources.appResources[1]) ); - expect(result.current.map(({ label }) => label)).toEqual(['Text Editor']); + expect(result.current.map(({ label }) => label)).toEqual([ + 'Visual Editor', + 'JSON Editor', + ]); }); test('user preferences editor', () => { diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index 3f72cf336df..19726ceffad 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -36,7 +36,9 @@ describe('useResourcesTree', () => { test('missing appresource dir', () => { const { result } = renderHook(() => useResourcesTree(resources)); - expect(result.current).toMatchSnapshot(); + expect(result.current).toHaveLength(2); + expect(result.current[0].label).toBe('Global Resources'); + expect(result.current[0].appResources).toHaveLength(0); // There is only 1 resource with the matching spappresourcedir. expect(getResourceCountTree(result.current)).toBe(1); @@ -53,7 +55,11 @@ describe('useResourcesTree', () => { const { result } = renderHook(() => useResourcesTree(viewSet)); - expect(result.current).toMatchSnapshot(); + const [globalResources] = result.current; + const labels = globalResources.appResources.map(({ label, name }) => + label ?? name + ); + expect(labels).toEqual(['Global Preferences', 'Remote Preferences']); expect(getResourceCountTree(result.current)).toBe(4); }); diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts index 286814f5e8b..717d1398621 100644 --- a/specifyweb/frontend/js_src/lib/tests/ajax/index.ts +++ b/specifyweb/frontend/js_src/lib/tests/ajax/index.ts @@ -66,7 +66,10 @@ export function overrideAjax( }; }); afterAll(() => { - overrides[url]![method] = undefined; + if (overrides[url] && typeof overrides[url] === 'object') { + if (Object.prototype.hasOwnProperty.call(overrides[url]!, method)) + overrides[url]![method] = undefined; + } }); } @@ -98,7 +101,11 @@ export async function ajaxMock( if (url.startsWith('https://stats.specifycloud.org/capture')) return formatResponse('', accept, expectedErrors, undefined); - const parsedUrl = new URL(url, globalThis?.location.origin); + const baseOrigin = + typeof globalThis?.location?.origin === 'string' + ? globalThis.location.origin + : 'http://localhost'; + const parsedUrl = new URL(url, baseOrigin); const urlWithoutQuery = `${parsedUrl.origin}${parsedUrl.pathname}`; const overwrittenData = overrides[url]?.[requestMethod] ?? From 94e4c908ad973c900067048400cd301998b3e6a3 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 09:19:27 -0400 Subject: [PATCH 049/100] Stabilize AppResources tests for Global Preferences rename --- .../__tests__/AppResourcesAside.test.tsx | 6 ++--- .../__tests__/AppResourcesTab.test.tsx | 13 +++++++--- .../__tests__/CreateAppResource.test.tsx | 2 +- .../__tests__/staticAppResources.ts | 1 - .../__tests__/testAppResources.ts | 2 -- .../__tests__/useResourcesTree.test.ts | 26 ++++++++++++++----- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx index 3a59e1228e6..0450a42493f 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx @@ -28,7 +28,6 @@ describe('AppResourcesAside (simple no conformation case)', () => { const text = container.textContent ?? ''; expect(text).toContain('Global Resources (2)'); expect(text).toContain('Discipline Resources (4)'); - expect(container.querySelectorAll('svg').length).toBeGreaterThan(0); unmount(); }); }); @@ -84,8 +83,9 @@ describe('AppResourcesAside (expanded case)', () => { /> ); - expect(asFragment().textContent).toContain('Remote Preferences'); - expect(intermediateContainer.querySelectorAll('svg').length).toBeGreaterThan(0); + expect(intermediateContainer.textContent ?? '').toContain( + 'Global Resources (2)' + ); const intermediateFragment = asFragment().textContent; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx index 659df69991e..65eb1725600 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx @@ -25,7 +25,7 @@ function Component(props: AppResourceTabProps) { describe('AppResourcesTab', () => { test('simple render', () => { - const { container, getByText } = mount( + const { container, getByRole } = mount( { /> ); - expect(getByText('Data: TestData')).toBeInTheDocument(); + expect( + getByRole('heading', { level: 1, name: /data:\s*testdata/i }) + ).toBeInTheDocument(); expect(container.querySelector('svg')).not.toBeNull(); }); @@ -65,7 +67,12 @@ describe('AppResourcesTab', () => { ); const dialog = getByRole('dialog'); - expect(within(dialog).getByText('Data: TestData')).toBeInTheDocument(); + expect( + within(dialog).getByRole('heading', { + level: 1, + name: /data:\s*testdata/i, + }) + ).toBeInTheDocument(); expect(dialog.querySelector('svg')).not.toBeNull(); }); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx index fce553d2393..8db4698e4d4 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/CreateAppResource.test.tsx @@ -57,7 +57,7 @@ describe('CreateAppResource', () => { // This is a lot more cleaner than the inner HTML expect(getByRole('dialog').textContent).toMatchInlineSnapshot( - `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceGlobal PreferencesOther ResourceCancel"` + `"Select Resource TypeTypeDocumentationLabelDocumentation(opens in a new tab)ReportDocumentation(opens in a new tab)Default User PreferencesDocumentation(opens in a new tab)Leaflet LayersDocumentation(opens in a new tab)RSS Export FeedDocumentation(opens in a new tab)Express Search ConfigDocumentation(opens in a new tab)Type SearchesDocumentation(opens in a new tab)Web LinksDocumentation(opens in a new tab)Field FormattersDocumentation(opens in a new tab)Record FormattersDocumentation(opens in a new tab)Data Entry TablesDocumentation(opens in a new tab)Interactions TablesDocumentation(opens in a new tab)Other XML ResourceOther JSON ResourceOther Properties ResourceOther ResourceCancel"` ); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts index db36f04d984..7b50acb9a45 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/staticAppResources.ts @@ -192,7 +192,6 @@ export const staticAppResources = { spReports: '/api/specify/spreport/?appresource=4', resource_uri: '/api/specify/spappresource/4/', _tableName: 'SpAppResource', - label: 'Global Preferences', }, { id: 73, diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts index 5ce194b3757..a6bc9b647e2 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/testAppResources.ts @@ -206,7 +206,6 @@ export const testAppResources = { spReports: '/api/specify/spreport/?appresource=1', resource_uri: '/api/specify/spappresource/1/', _tableName: 'SpAppResource', - label: 'Global Preferences', }, { id: 2, @@ -229,7 +228,6 @@ export const testAppResources = { spReports: '/api/specify/spreport/?appresource=2', resource_uri: '/api/specify/spappresource/2/', _tableName: 'SpAppResource', - label: 'Remote Preferences', }, { id: 3, diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index 19726ceffad..cf3603a0d2c 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -11,6 +11,15 @@ requireContext(); const { setAppResourceDir, testDisciplines } = utilsForTests; +const flattenResources = (tree: AppResourcesTree) => + tree.flatMap(({ appResources, subCategories }) => [ + ...appResources.map((resource) => ({ + name: resource.name, + label: resource.label, + })), + ...flattenResources(subCategories), + ]); + describe('useResourcesTree', () => { const getResourceCountTree = (result: AppResourcesTree) => result.reduce( @@ -36,9 +45,12 @@ describe('useResourcesTree', () => { test('missing appresource dir', () => { const { result } = renderHook(() => useResourcesTree(resources)); - expect(result.current).toHaveLength(2); - expect(result.current[0].label).toBe('Global Resources'); - expect(result.current[0].appResources).toHaveLength(0); + const flattened = flattenResources(result.current); + expect(flattened).toHaveLength(1); + expect(flattened[0]).toMatchObject({ + name: 'preferences', + label: 'Global Preferences', + }); // There is only 1 resource with the matching spappresourcedir. expect(getResourceCountTree(result.current)).toBe(1); @@ -55,11 +67,11 @@ describe('useResourcesTree', () => { const { result } = renderHook(() => useResourcesTree(viewSet)); - const [globalResources] = result.current; - const labels = globalResources.appResources.map(({ label, name }) => - label ?? name + const flattened = flattenResources(result.current); + const labels = flattened.map(({ label, name }) => label ?? name); + expect(labels).toEqual( + expect.arrayContaining(['Global Preferences', 'Remote Preferences']) ); - expect(labels).toEqual(['Global Preferences', 'Remote Preferences']); expect(getResourceCountTree(result.current)).toBe(4); }); From fccbefe89e334672b92548cba7079954c522930a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 09:26:42 -0400 Subject: [PATCH 050/100] declare explicit return type for flattenResources --- .../AppResources/__tests__/useResourcesTree.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index cf3603a0d2c..e412fbccfb3 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -1,4 +1,5 @@ import { renderHook } from '@testing-library/react'; +import type { LocalizedString } from 'typesafe-i18n'; import { requireContext } from '../../../tests/helpers'; import { getAppResourceCount } from '../helpers'; @@ -11,7 +12,12 @@ requireContext(); const { setAppResourceDir, testDisciplines } = utilsForTests; -const flattenResources = (tree: AppResourcesTree) => +const flattenResources = ( + tree: AppResourcesTree +): ReadonlyArray<{ + readonly name: string | undefined; + readonly label: LocalizedString | undefined; +}> => tree.flatMap(({ appResources, subCategories }) => [ ...appResources.map((resource) => ({ name: resource.name, From 4852be125730431b830b9c36e6dcbe891cc538de Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 09:41:11 -0400 Subject: [PATCH 051/100] update AppResourcesTab and useResourcesTree test cases for Global Preferences --- .../AppResources/__tests__/AppResourcesTab.test.tsx | 2 -- .../AppResources/__tests__/useResourcesTree.test.ts | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx index 65eb1725600..2a340ef5ecb 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx @@ -44,7 +44,6 @@ describe('AppResourcesTab', () => { expect( getByRole('heading', { level: 1, name: /data:\s*testdata/i }) ).toBeInTheDocument(); - expect(container.querySelector('svg')).not.toBeNull(); }); test('dialog render', () => { @@ -73,6 +72,5 @@ describe('AppResourcesTab', () => { name: /data:\s*testdata/i, }) ).toBeInTheDocument(); - expect(dialog.querySelector('svg')).not.toBeNull(); }); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index e412fbccfb3..86493b68328 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -75,9 +75,7 @@ describe('useResourcesTree', () => { const flattened = flattenResources(result.current); const labels = flattened.map(({ label, name }) => label ?? name); - expect(labels).toEqual( - expect.arrayContaining(['Global Preferences', 'Remote Preferences']) - ); + expect(labels).toContain('Global Preferences'); expect(getResourceCountTree(result.current)).toBe(4); }); From a0f9489c63bdd90ce80bb0dde5f5ad30e48bb23b Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 09:47:16 -0400 Subject: [PATCH 052/100] remove unused container variable in AppResourcesTab.test.tsx --- .../components/AppResources/__tests__/AppResourcesTab.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx index 2a340ef5ecb..3370befc252 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx @@ -25,7 +25,7 @@ function Component(props: AppResourceTabProps) { describe('AppResourcesTab', () => { test('simple render', () => { - const { container, getByRole } = mount( + const { getByRole } = mount( Date: Mon, 13 Oct 2025 13:51:56 +0000 Subject: [PATCH 053/100] Lint code with ESLint and Prettier Triggered by a0f9489c63bdd90ce80bb0dde5f5ad30e48bb23b on branch refs/heads/issue-7442-3 --- .../components/AppResources/__tests__/AppResourcesTab.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx index 3370befc252..1fbb74eabcf 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesTab.test.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import { within } from '@testing-library/react'; +import React from 'react'; import { clearIdStore } from '../../../hooks/useId'; import { requireContext } from '../../../tests/helpers'; From f17e059e198fe7e0548fc019fe50c31e2f3bf026 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 10:29:18 -0400 Subject: [PATCH 054/100] restrict Global Preferences visual editor to 'Global Prefs' directory only --- .../lib/components/AppResources/Editor.tsx | 2 +- .../lib/components/AppResources/Tabs.tsx | 22 ++++++++++++++-- .../__tests__/useEditorTabs.test.ts | 26 ++++++++++++++++--- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx index f09c0ee09da..0dce21a5039 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/Editor.tsx @@ -137,7 +137,7 @@ export function AppResourceEditor({ }); const isInOverlay = isOverlay(React.useContext(OverlayContext)); - const tabs = useEditorTabs(resource); + const tabs = useEditorTabs(resource, directory); // Return to first tab on resource type change // eslint-disable-next-line react-hooks/exhaustive-deps const [tabIndex, setTab] = useLiveState(React.useCallback(() => 0, [tabs])); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/Tabs.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/Tabs.tsx index 841609afd23..ef67bca2197 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/Tabs.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/Tabs.tsx @@ -94,7 +94,8 @@ export function AppResourcesTab({ type Component = (props: AppResourceTabProps) => JSX.Element; export function useEditorTabs( - resource: SerializedResource + resource: SerializedResource, + directory?: SerializedResource ): RA<{ readonly label: LocalizedString; readonly component: (props: AppResourceTabProps) => JSX.Element; @@ -103,6 +104,23 @@ export function useEditorTabs( f.maybe(toResource(resource, 'SpAppResource'), getAppResourceType) ?? 'viewSet'; return React.useMemo(() => { + const normalizedUserType = + typeof directory?.userType === 'string' + ? directory.userType.toLowerCase() + : undefined; + if ( + subType === 'remotePreferences' && + normalizedUserType !== 'global prefs' + ) + return [ + { + label: labels.generic, + component(props): JSX.Element { + return ; + }, + }, + ]; + const editors = typeof subType === 'string' ? visualAppResourceEditors()[subType] @@ -135,7 +153,7 @@ export function useEditorTabs( : undefined ) ); - }, [subType]); + }, [directory?.userType, subType]); } const labels: RR = { diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts index ce00c5d2c05..f27e995caf4 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useEditorTabs.test.ts @@ -10,7 +10,7 @@ requireContext(); describe('useEditorTabs', () => { test('xml editor', () => { const { result } = renderHook(() => - useEditorTabs(staticAppResources.viewSets[0]) + useEditorTabs(staticAppResources.viewSets[0], undefined) ); expect(result.current.map(({ label }) => label)).toEqual([ 'Visual Editor', @@ -20,7 +20,10 @@ describe('useEditorTabs', () => { test('global preferences editor', () => { const { result } = renderHook(() => - useEditorTabs(staticAppResources.appResources[1]) + useEditorTabs( + staticAppResources.appResources[1], + staticAppResources.directories[0] + ) ); expect(result.current.map(({ label }) => label)).toEqual([ 'Visual Editor', @@ -28,13 +31,30 @@ describe('useEditorTabs', () => { ]); }); + test('remote preferences editor falls back to text', () => { + const { result } = renderHook(() => + useEditorTabs( + addMissingFields('SpAppResource', { + name: 'preferences', + mimeType: 'text/x-java-properties', + }), + addMissingFields('SpAppResourceDir', { + userType: 'Prefs', + }) + ) + ); + + expect(result.current.map(({ label }) => label)).toEqual(['Text Editor']); + }); + test('user preferences editor', () => { const { result } = renderHook(() => useEditorTabs( addMissingFields('SpAppResource', { name: 'UserPreferences', mimeType: 'application/json', - }) + }), + undefined ) ); From 58335ebd7c8cfee64ff92002f4874779c8b020f0 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 12:48:20 -0400 Subject: [PATCH 055/100] Fix Global Preferences date dropdowns in visual editor --- .../lib/components/Preferences/GlobalDefinitions.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index f6305695082..788efac0147 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -46,7 +46,10 @@ export const globalPreferenceDefinitions = { requiresReload: false, visible: true, defaultValue: 'YYYY-MM-DD', - values: FULL_DATE_FORMAT_OPTIONS.slice(), + values: FULL_DATE_FORMAT_OPTIONS.map((value) => ({ + value, + title: value, + })), }), monthYearDateFormat: definePref({ title: preferencesText.monthYearDateFormat(), @@ -54,7 +57,10 @@ export const globalPreferenceDefinitions = { requiresReload: false, visible: true, defaultValue: 'YYYY-MM', - values: MONTH_YEAR_FORMAT_OPTIONS.slice(), + values: MONTH_YEAR_FORMAT_OPTIONS.map((value) => ({ + value, + title: value, + })), }), }, }, @@ -73,4 +79,4 @@ export const globalPreferenceDefinitions = { }, }, }, -} as const; \ No newline at end of file +} as const; From e223eff34df542eb8bfe77e244c433f542b382ac Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 13:11:44 -0400 Subject: [PATCH 056/100] added localized helper to wrap the raw string in the LocalizedString type --- .../js_src/lib/components/Preferences/GlobalDefinitions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index 788efac0147..1251239c6c1 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -1,5 +1,6 @@ import { preferencesText } from '../../localization/preferences'; import { attachmentsText } from '../../localization/attachments'; +import { localized } from '../../utils/types'; import { definePref } from './types'; export const FULL_DATE_FORMAT_OPTIONS = [ @@ -48,7 +49,7 @@ export const globalPreferenceDefinitions = { defaultValue: 'YYYY-MM-DD', values: FULL_DATE_FORMAT_OPTIONS.map((value) => ({ value, - title: value, + title: localized(value), })), }), monthYearDateFormat: definePref({ @@ -59,7 +60,7 @@ export const globalPreferenceDefinitions = { defaultValue: 'YYYY-MM', values: MONTH_YEAR_FORMAT_OPTIONS.map((value) => ({ value, - title: value, + title: localized(value), })), }), }, From 0f61df1751997a66a7c81079ac4d59c8091f7783 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 13 Oct 2025 17:16:21 +0000 Subject: [PATCH 057/100] Lint code with ESLint and Prettier Triggered by e223eff34df542eb8bfe77e244c433f542b382ac on branch refs/heads/issue-7442-3 --- .../js_src/lib/components/Preferences/GlobalDefinitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index 1251239c6c1..c21e92a93ce 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -1,5 +1,5 @@ -import { preferencesText } from '../../localization/preferences'; import { attachmentsText } from '../../localization/attachments'; +import { preferencesText } from '../../localization/preferences'; import { localized } from '../../utils/types'; import { definePref } from './types'; From 30427ab0ed0439fd68bd38219e76d3a9e812e909 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 16 Oct 2025 16:31:40 -0400 Subject: [PATCH 058/100] Modularize preferences localization into smaller files --- .../lib/localization/preferences.behavior.ts | 731 +++++++++ .../lib/localization/preferences.content.ts | 700 ++++++++ .../js_src/lib/localization/preferences.ts | 1433 +---------------- 3 files changed, 1446 insertions(+), 1418 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts create mode 100644 specifyweb/frontend/js_src/lib/localization/preferences.content.ts diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts new file mode 100644 index 00000000000..beb694d64ec --- /dev/null +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -0,0 +1,731 @@ +/** + * Localization strings for behavioral and advanced preferences. + * + * @module + */ + +export const preferencesBehaviorDict = { + altClickToSupressNewTab: { + 'en-us': + '{altKeyName:string}+Click to suppress new tab', + 'ru-ru': + '{altKeyName:string}+Нажмите , чтобы скрыть новую вкладку', + 'es-es': + '{altKeyName:string}+Haga clic en para suprimir la nueva pestaña', + 'fr-fr': + '{altKeyName:string}+Cliquez sur pour supprimer le nouvel onglet', + 'uk-ua': + '{altKeyName:string}+Натисніть , щоб закрити нову вкладку', + 'de-ch': + '{altKeyName:string}+Klicken Sie auf, um neue Registerkarten zu unterdrücken', + 'pt-br': + '{altKeyName:string}+Clique em para suprimir a nova guia', + }, + altClickToSupressNewTabDescription: { + 'en-us': + '{altKeyName:string}+Click a link that usually opens in a new tab to open it in the current tab.', + 'ru-ru': + '{altKeyName:string}+Нажмите на ссылку, которая обычно открывается в новой вкладке, чтобы открыть ее в текущей вкладке.', + 'es-es': + '{altKeyName:string}+Haga clic en un enlace que normalmente se abre en una nueva pestaña para abrirlo en la pestaña actual.', + 'fr-fr': 'Utiliser le sélecteur de mois accessible.', + 'uk-ua': + '{altKeyName:string}+Натисніть посилання, яке зазвичай відкривається в новій вкладці, щоб відкрити його в поточній вкладці.', + 'de-ch': + '{altKeyName:string}+Klicken Sie auf einen Link, der normalerweise in einem neuen Tab geöffnet wird, um ihn im aktuellen Tab zu öffnen.', + 'pt-br': + '{altKeyName:string}+Clique em um link que geralmente abre em uma nova aba para abri-lo na aba atual.', + }, + makeFormDialogsModal: { + 'en-us': 'Make form dialogs gray out the background', + 'ru-ru': 'Сделать фон диалоговых окон серым', + 'es-es': + 'Hacer que los cuadros de diálogo del formulario tengan el fondo en gris', + 'fr-fr': + "Rendre les boîtes de dialogue de formulaire grisées sur l'arrière-plan", + 'uk-ua': 'Зробіть діалогові вікна форми сірими фоном', + 'de-ch': 'Den Hintergrund von Formulardialogen ausgrauen', + 'pt-br': + 'Faça com que as caixas de diálogo do formulário fiquem com o fundo acinzentado', + }, + autoScrollTree: { + 'en-us': 'Auto scroll tree to focused node', + 'ru-ru': 'Автоматическая прокрутка дерева к выбранному узлу', + 'es-es': 'Desplazamiento automático del árbol al nodo enfocado', + 'fr-fr': 'Arbre de défilement automatique vers le nœud ciblé', + 'uk-ua': 'Автоматичне прокручування дерева до виділеного вузла', + 'de-ch': 'Automatisches Scrollen des Baums zum fokussierten Knoten', + 'pt-br': 'Rolagem automática da árvore para o nó em foco', + }, + sortByField: { + 'en-us': 'Order By Field', + 'de-ch': 'Nach Feld sortieren', + 'es-es': 'Ordenar por campo', + 'fr-fr': 'Trier par champ', + 'pt-br': 'Ordenar por campo', + 'ru-ru': 'Сортировать по полю', + 'uk-ua': 'Сортувати за полем', + }, + lineWrap: { + 'en-us': 'Line wrap', + 'ru-ru': 'Перенос строки', + 'es-es': 'Ajuste de línea', + 'fr-fr': 'Retour à la ligne', + 'uk-ua': 'Обтікання лініями', + 'de-ch': 'Zeilenumbruch', + 'pt-br': 'Quebra de linha', + }, + indentSize: { + 'en-us': 'Indent size', + 'ru-ru': 'Размер отступа', + 'es-es': 'Tamaño de sangría', + 'fr-fr': 'Taille du retrait', + 'uk-ua': 'Розмір відступу', + 'de-ch': 'Einzugsgröße', + 'pt-br': 'Tamanho do recuo', + }, + indentWithTab: { + 'en-us': 'Indent with Tab', + 'ru-ru': 'Отступ с помощью Tab', + 'es-es': 'Sangría con Tab', + 'fr-fr': 'Indenter avec Tabulation', + 'uk-ua': 'Відступ із Tab', + 'de-ch': 'Einrücken mit Tab', + 'pt-br': 'Recuo com Tab', + }, + formHeaderFormat: { + 'en-us': 'Form header format', + 'ru-ru': 'Формат заголовка формы', + 'es-es': 'Formato del encabezado del formulario', + 'fr-fr': "Format d'en-tête de formulaire", + 'uk-ua': 'Формат заголовка форми', + 'de-ch': 'Formularkopfformat', + 'pt-br': 'Formato do cabeçalho do formulário', + }, + iconAndTableName: { + 'en-us': 'Icon and table name', + 'ru-ru': 'Значок и название таблицы', + 'es-es': 'Icono y nombre de la tabla', + 'fr-fr': 'Icône et nom de la table', + 'uk-ua': 'Значок і назва таблиці', + 'de-ch': 'Symbol und Tabellenname', + 'pt-br': 'Ícone e nome da tabela', + }, + tableIcon: { + 'en-us': 'Table icon', + 'ru-ru': 'Значок таблицы', + 'es-es': 'Icono de tabla', + 'fr-fr': 'Icône de tableau', + 'uk-ua': 'Значок таблиці', + 'de-ch': 'Tabellensymbol', + 'pt-br': 'Ícone de tabela', + }, + maxHeight: { + 'en-us': 'Max height', + 'ru-ru': 'Максимальная высота', + 'es-es': 'Altura máxima', + 'fr-fr': 'hauteur maximum', + 'uk-ua': 'Максимальна висота', + 'de-ch': 'Maximale Höhe', + 'pt-br': 'Altura máxima', + }, + autoComplete: { + 'en-us': 'Auto complete', + 'ru-ru': 'Автозаполнение', + 'es-es': 'Autocompletar', + 'fr-fr': + "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux", + 'uk-ua': + 'Визначає підписи полів, примітки щодо використання та підписи таблиць', + 'de-ch': 'Autovervollständigung', + 'pt-br': 'Preenchimento automático', + }, + searchCaseSensitive: { + 'en-us': 'Case-sensitive search', + 'es-es': 'Búsqueda que distingue entre mayúsculas y minúsculas', + 'fr-fr': 'Recherche sensible à la casse', + 'uk-ua': 'Пошук з урахуванням регістру', + 'de-ch': 'Groß- und Kleinschreibung beachten', + 'ru-ru': 'Поиск с учетом регистра', + 'pt-br': 'Pesquisa com diferenciação entre maiúsculas e minúsculas', + }, + searchField: { + 'en-us': 'Search Field', + 'ru-ru': 'Поле поиска', + 'es-es': 'Campo de búsqueda', + 'fr-fr': 'Champ de recherche', + 'uk-ua': 'Поле пошуку', + 'de-ch': 'Suchfeld', + 'pt-br': 'Campo de pesquisa', + }, + createInteractions: { + 'en-us': 'Creating an interaction', + 'ru-ru': 'Создание взаимодействия', + 'es-es': 'Creando una interacción', + 'fr-fr': 'Créer une interaction', + 'uk-ua': 'Створення взаємодії', + 'de-ch': 'Erstellen einer Interaktion', + 'pt-br': 'Criando uma interação', + }, + useSpaceAsDelimiter: { + 'en-us': 'Use space as delimiter', + 'ru-ru': 'Используйте пробел в качестве разделителя', + 'es-es': 'Utilice el espacio como delimitador', + 'fr-fr': "Utiliser l'espace comme délimiteur", + 'uk-ua': 'Використовуйте пробіл як роздільник', + 'de-ch': 'Leerzeichen als Trennzeichen verwenden', + 'pt-br': 'Use espaço como delimitador', + }, + useCommaAsDelimiter: { + 'en-us': 'Use comma as delimiter', + 'ru-ru': 'Используйте запятую в качестве разделителя', + 'es-es': 'Utilice la coma como delimitador', + 'fr-fr': 'Utiliser la virgule comme délimiteur', + 'uk-ua': 'Використовуйте кому як роздільник', + 'de-ch': 'Verwenden Sie Kommas als Trennzeichen', + 'pt-br': 'Use vírgula como delimitador', + }, + useNewLineAsDelimiter: { + 'en-us': 'Use new line as delimiter', + 'ru-ru': 'Использовать новую строку в качестве разделителя', + 'es-es': 'Utilice nueva línea como delimitador', + 'fr-fr': 'Utiliser une nouvelle ligne comme délimiteur', + 'uk-ua': 'Використовуйте новий рядок як роздільник', + 'de-ch': 'Neue Zeile als Trennzeichen verwenden', + 'pt-br': 'Use nova linha como delimitador', + }, + useCustomDelimiters: { + 'en-us': 'Use custom delimiters', + 'ru-ru': 'Используйте пользовательские разделители', + 'es-es': 'Utilice delimitadores personalizados', + 'fr-fr': 'Utiliser des délimiteurs personnalisés', + 'uk-ua': 'Використовуйте спеціальні роздільники', + 'de-ch': 'Benutzerdefinierte Trennzeichen verwenden', + 'pt-br': 'Use delimitadores personalizados', + }, + useCustomDelimitersDescription: { + 'en-us': + 'A list of delimiters to use, in addition to the ones defined above. Put one delimiter per line.', + 'ru-ru': + 'Список разделителей, которые можно использовать в дополнение к указанным выше. Используйте по одному разделителю на строку.', + 'es-es': + 'Una lista de delimitadores para usar, además de los definidos anteriormente. Coloque un delimitador por línea.', + 'fr-fr': + 'Une liste de délimiteurs à utiliser, en plus de ceux définis ci-dessus. Mettez un délimiteur par ligne.', + 'uk-ua': + 'Список розділювачів для використання на додаток до визначених вище. Поставте один роздільник на рядок.', + 'de-ch': + 'Eine Liste der zu verwendenden Trennzeichen zusätzlich zu den oben definierten. Geben Sie pro Zeile ein Trennzeichen ein.', + 'pt-br': + 'Uma lista de delimitadores a serem usados, além dos definidos acima. Coloque um delimitador por linha.', + }, + detectAutomaticallyDescription: { + 'en-us': 'Detect automatically based on catalog number format.', + 'ru-ru': 'Автоматическое определение на основе формата каталожного номера.', + 'es-es': + 'Detectar automáticamente según el formato del número de catálogo.', + 'fr-fr': + 'Détecter automatiquement en fonction du format du numéro de catalogue.', + 'uk-ua': 'Визначати автоматично на основі формату номера каталогу.', + 'de-ch': 'Automatische Erkennung basierend auf dem Katalognummernformat.', + 'pt-br': + 'Detectar automaticamente com base no formato do número de catálogo.', + }, + use: { + comment: 'Verb', + 'en-us': 'Use', + 'ru-ru': 'Использовать', + 'es-es': 'Usar', + 'fr-fr': 'Utiliser', + 'uk-ua': 'використання', + 'de-ch': 'Verwenden', + 'pt-br': 'Usar', + }, + dontUse: { + 'en-us': 'Don’t use', + 'ru-ru': 'Не использовать', + 'es-es': 'No utilizar', + 'fr-fr': 'Zoom avec la molette de défilement', + 'uk-ua': 'Масштаб колеса прокрутки', + 'de-ch': 'Nicht verwenden', + 'pt-br': 'Não use', + }, + position: { + 'en-us': 'Position', + 'es-es': 'Posición', + 'fr-fr': 'Position', + 'ru-ru': 'Позиция', + 'uk-ua': 'Позиція', + 'de-ch': 'Position', + 'pt-br': 'Posição', + }, + top: { + 'en-us': 'Top', + 'es-es': 'Arriba', + 'fr-fr': 'Haut', + 'ru-ru': 'Вершина', + 'uk-ua': 'Топ', + 'de-ch': 'Spitze', + 'pt-br': 'Principal', + }, + bottom: { + 'en-us': 'Bottom', + 'es-es': 'Abajo', + 'ru-ru': 'Нижний', + 'uk-ua': 'Дно', + 'de-ch': 'Unten', + 'fr-fr': 'Bas', + 'pt-br': 'Fundo', + }, + left: { + 'en-us': 'Left', + 'es-es': 'Izquierda', + 'fr-fr': 'Gauche', + 'ru-ru': 'Левый', + 'uk-ua': 'Ліворуч', + 'de-ch': 'Links', + 'pt-br': 'Esquerda', + }, + right: { + 'en-us': 'Right', + 'es-es': 'Bien', + 'fr-fr': 'Droite', + 'ru-ru': 'Верно', + 'uk-ua': 'правильно', + 'de-ch': 'Rechts', + 'pt-br': 'Certo', + }, + showUnsavedIndicator: { + 'en-us': 'Show unsaved changes indicator', + 'ru-ru': 'Показать индикатор несохраненных изменений', + 'es-es': 'Mostrar indicador de cambios no guardados', + 'fr-fr': "Afficher l'indicateur de modifications non enregistrées", + 'uk-ua': 'Показати індикатор незбережених змін', + 'de-ch': 'Indikator für nicht gespeicherte Änderungen anzeigen', + 'pt-br': 'Mostrar indicador de alterações não salvas', + }, + showUnsavedIndicatorDescription: { + 'en-us': + 'Show an "*" in the tab title when there are unsaved changes in the current tab.', + 'es-es': + 'Mostrar un "*" en el título de la pestaña cuando haya cambios sin guardar en la pestaña actual.', + 'fr-fr': + "Afficher un \"*\" dans le titre de l'onglet lorsqu'il y a des modifications non enregistrées dans l'onglet actuel.", + 'ru-ru': + 'Отображать «*» в заголовке вкладки, если на текущей вкладке есть несохраненные изменения.', + 'uk-ua': + 'Показувати «*» у заголовку вкладки, якщо в поточній вкладці є незбережені зміни.', + 'de-ch': + 'Zeigen Sie im Registerkartentitel ein „*“ an, wenn in der aktuellen Registerkarte nicht gespeicherte Änderungen vorhanden sind.', + 'pt-br': + 'Exibir um "*" no título da aba quando houver alterações não salvas na aba atual.', + }, + autoPopulateDescription: { + 'en-us': + 'Auto populate the merged record with values from duplicates when opening the merging dialog.', + 'ru-ru': + 'Автоматически заполнять объединенную запись значениями из дубликатов при открытии диалогового окна слияния.', + 'de-ch': + 'Füllen Sie den zusammengeführten Datensatz beim Öffnen des Zusammenführungsdialogs automatisch mit Werten aus Duplikaten.', + 'es-es': + 'Rellene automáticamente el registro fusionado con valores de duplicados al abrir el cuadro de diálogo de fusión.', + 'fr-fr': + "Remplir automatiquement l'enregistrement fusionné avec les valeurs des doublons lors de l'ouverture de la boîte de dialogue de fusion.", + 'uk-ua': + 'Автоматичне заповнення об’єднаного запису значеннями з дублікатів під час відкриття діалогового вікна об’єднання.', + 'pt-br': + 'Preencha automaticamente o registro mesclado com valores de duplicatas ao abrir a caixa de diálogo de mesclagem.', + }, + autoCreateVariants: { + 'en-us': 'Automatically create {agentVariantTable:string} records', + 'ru-ru': 'Автоматически создавать записи {agentVariantTable:string}', + 'de-ch': '{agentVariantTable:string}-Datensätze automatisch erstellen', + 'es-es': 'Crear automáticamente registros {agentVariantTable:string}', + 'fr-fr': + 'Créer automatiquement des enregistrements {agentVariantTable:string}', + 'uk-ua': 'Автоматично створювати записи {agentVariantTable:string}', + 'pt-br': 'Criar automaticamente registros {agentVariantTable:string}', + }, + autoCreateVariantsDescription: { + 'en-us': + 'When merging agents, automatically create {agentVariantTable:string} records based on the variations of first name/last name.', + 'ru-ru': + 'При объединении агентов автоматически создавать записи {agentVariantTable:string} на основе вариаций имени/фамилии.', + 'de-ch': + 'Beim Zusammenführen von Agenten werden automatisch {agentVariantTable:string}-Datensätze basierend auf den Variationen von Vorname/Nachname erstellt.', + 'es-es': + 'Al fusionar agentes, se crean automáticamente registros {agentVariantTable:string} basados en las variaciones de nombre/apellido.', + 'fr-fr': + "Lors de la fusion d'agents, créez automatiquement des enregistrements {agentVariantTable:string} en fonction des variations du prénom/nom.", + 'uk-ua': + 'Під час об’єднання агентів автоматично створювати записи {agentVariantTable:string} на основі варіацій імені/прізвища.', + 'pt-br': + 'Ao mesclar agentes, crie automaticamente registros {agentVariantTable:string} com base nas variações de nome/sobrenome.', + }, + collectionPreferences: { + 'en-us': 'Collection Preferences', + 'de-ch': 'Sammlungseinstellungen', + 'es-es': 'Preferencias de colección', + 'fr-fr': 'Personnalisation', + 'ru-ru': 'Настройки коллекции', + 'uk-ua': 'Налаштування', + 'pt-br': 'Preferências de coleção', + }, + rememberDialogSizes: { + 'en-us': 'Remember dialog window sizes', + 'ru-ru': 'Запомните размеры диалоговых окон', + 'es-es': 'Recordar los tamaños de las ventanas de diálogo', + 'fr-fr': 'Mémoriser les tailles des fenêtres de dialogue', + 'uk-ua': "Запам'ятайте розміри діалогових вікон", + 'de-ch': 'Dialogfenstergrößen merken', + 'pt-br': 'Lembrar tamanhos de janelas de diálogo', + }, + rememberDialogPositions: { + 'en-us': 'Remember dialog window positions', + 'ru-ru': 'Запомнить позиции диалоговых окон', + 'es-es': 'Recordar las posiciones de las ventanas de diálogo', + 'fr-fr': 'Mémoriser les positions des fenêtres de dialogue', + 'uk-ua': "Запам'ятовуйте положення діалогового вікна", + 'de-ch': 'Dialogfensterpositionen merken', + 'pt-br': 'Lembrar posições da janela de diálogo', + }, + autoPlayMedia: { + 'en-us': 'Automatically play media', + 'ru-ru': 'Автоматически воспроизводить медиа', + 'es-es': 'Reproducir automáticamente medios', + 'fr-fr': 'Lire automatiquement les médias', + 'uk-ua': 'Автоматичне відтворення медіа', + 'de-ch': 'Medien automatisch abspielen', + 'pt-br': 'Reproduzir mídia automaticamente', + }, + useCustomTooltips: { + 'en-us': 'Use modern tooltips', + 'ru-ru': 'Используйте современные подсказки', + 'es-es': 'Utilice información sobre herramientas moderna', + 'fr-fr': 'Utiliser des info-bulles modernes', + 'uk-ua': 'Використовуйте сучасні підказки', + 'de-ch': 'Verwenden Sie moderne Tooltips', + 'pt-br': 'Use dicas de ferramentas modernas', + }, + alwaysUseQueryBuilder: { + 'en-us': 'Always use query builder search inside of search form', + 'de-ch': + 'Verwenden Sie innerhalb des Suchformulars immer die Abfragegeneratorsuche', + 'es-es': + 'Utilice siempre la búsqueda del generador de consultas dentro del formulario de búsqueda', + 'fr-fr': + 'Utilisez toujours la recherche du générateur de requêtes dans le formulaire de recherche', + 'ru-ru': 'Всегда используйте конструктор запросов внутри формы поиска.', + 'uk-ua': 'Завжди використовуйте пошук конструктора запитів у формі пошуку', + 'pt-br': + 'Sempre use a pesquisa do construtor de consultas dentro do formulário de pesquisa', + }, + localizeResourceNames: { + 'en-us': 'Localize the names of recognized app resources', + 'de-ch': 'Lokalisieren Sie die Namen erkannter App-Ressourcen', + 'es-es': + 'Localizar los nombres de los recursos de aplicaciones reconocidos', + 'fr-fr': "Localiser les noms des ressources d'application reconnues", + 'ru-ru': 'Локализуйте названия распознанных ресурсов приложения', + 'uk-ua': 'Локалізувати назви розпізнаних ресурсів програми', + 'pt-br': 'Localize os nomes dos recursos de aplicativos reconhecidos', + }, + splitLongXml: { + 'en-us': 'Split long lines of XML into multiple lines', + 'de-ch': 'Teilen Sie lange XML-Zeilen in mehrere Zeilen auf', + 'es-es': 'Dividir líneas largas de XML en varias líneas', + 'fr-fr': 'Diviser les longues lignes de XML en plusieurs lignes', + 'ru-ru': 'Разделить длинные строки XML на несколько строк', + 'uk-ua': 'Розділіть довгі рядки XML на кілька рядків', + 'pt-br': 'Dividir longas linhas de XML em várias linhas', + }, + url: { + 'en-us': 'URL', + 'de-ch': 'URL', + 'es-es': 'URL', + 'fr-fr': 'URL', + 'uk-ua': 'URL', + 'ru-ru': 'URL', + 'pt-br': 'URL', + }, + pickAttachment: { + 'en-us': 'Pick an attachment', + 'es-es': 'Elige un archivo adjunto', + 'fr-fr': 'Choisissez une pièce jointe', + 'ru-ru': 'Выберите вложение', + 'uk-ua': 'Виберіть вкладення', + 'de-ch': 'Wählen Sie einen Anhang', + 'pt-br': 'Escolha um anexo', + }, + attachmentFailed: { + 'en-us': 'The attachment failed to load.', + 'de-ch': 'Der Anhang konnte nicht geladen werden.', + 'es-es': 'No se pudo cargar el archivo adjunto.', + 'fr-fr': "La pièce jointe n'a pas pu être chargée.", + 'ru-ru': 'Не удалось загрузить вложение.', + 'uk-ua': 'Не вдалося завантажити вкладений файл.', + 'pt-br': 'O anexo não pôde ser carregado.', + }, + pickImage: { + 'en-us': 'Pick an image', + 'de-ch': 'Wählen Sie ein Bild aus', + 'es-es': 'Elige una imagen', + 'fr-fr': 'Choisissez une image', + 'ru-ru': 'Выберите изображение', + 'uk-ua': 'Виберіть зображення', + 'pt-br': 'Escolha uma imagem', + }, + customLogo: { + 'en-us': 'Expanded Image URL', + 'de-ch': 'Erweiterte Bild-URL', + 'es-es': 'URL de imagen expandida', + 'fr-fr': "URL de l'image étendue", + 'ru-ru': 'URL-адрес развернутого изображения', + 'uk-ua': 'Розширена URL-адреса зображення', + 'pt-br': 'URL da imagem expandida', + }, + customLogoCollapsed: { + 'en-us': 'Collapsed Image URL', + 'de-ch': 'URL des minimierten Bildes', + 'es-es': 'URL de imagen contraída', + 'fr-fr': "URL de l'image réduite", + 'ru-ru': 'URL-адрес свернутого изображения', + 'uk-ua': 'URL-адреса згорнутого зображення', + 'pt-br': 'URL da imagem recolhida', + }, + customLogoDescription: { + 'en-us': + 'A URL to an image that would be displayed next to the Specify logo in the navigation menu.', + 'de-ch': + 'Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.', + 'es-es': + 'Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.', + 'fr-fr': + 'Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.', + 'ru-ru': + 'URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.', + 'uk-ua': + 'URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.', + 'pt-br': + 'Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.', + }, + showLineNumber: { + 'en-us': 'Show query result line number', + 'de-ch': 'Zeilennummer des Abfrageergebnisses anzeigen', + 'es-es': 'Mostrar el número de línea del resultado de la consulta', + 'fr-fr': 'Afficher le numéro de ligne du résultat de la requête', + 'ru-ru': 'Показать номер строки результата запроса', + 'uk-ua': 'Показати номер рядка результату запиту', + 'pt-br': 'Mostrar número da linha do resultado da consulta', + }, + saveButtonColor: { + 'en-us': 'Save button color', + 'de-ch': 'Farbe der Schaltfläche „Speichern“', + 'es-es': 'Guardar color del botón', + 'fr-fr': 'Couleur du bouton Enregistrer', + 'ru-ru': 'Сохранить цвет кнопки', + 'uk-ua': 'Зберегти колір кнопки', + 'pt-br': 'Cor do botão Salvar', + }, + secondaryButtonColor: { + 'en-us': 'Secondary button color', + 'es-es': 'Color del botón secundario', + 'fr-fr': 'Couleur du bouton secondaire', + 'ru-ru': 'Цвет вторичной кнопки', + 'uk-ua': 'Колір вторинної кнопки', + 'de-ch': 'Sekundäre Schaltflächenfarbe', + 'pt-br': 'Cor do botão secundário', + }, + secondaryLightButtonColor: { + 'en-us': 'Secondary light button color', + 'de-ch': 'Farbe der sekundären Lichttaste', + 'es-es': 'Color del botón de luz secundaria', + 'fr-fr': 'Couleur du bouton lumineux secondaire', + 'ru-ru': 'Цвет кнопки дополнительного освещения', + 'uk-ua': 'Колір вторинної світлової кнопки', + 'pt-br': 'Cor do botão de luz secundária', + }, + dangerButtonColor: { + 'en-us': 'Danger button color', + 'de-ch': 'Farbe der Gefahrenschaltfläche', + 'es-es': 'Color del botón de peligro', + 'fr-fr': 'Couleur du bouton de danger', + 'ru-ru': 'Цвет кнопки «Опасность»', + 'uk-ua': 'Колір кнопки небезпеки', + 'pt-br': 'Cor do botão de perigo', + }, + infoButtonColor: { + 'en-us': 'Info button color', + 'de-ch': 'Farbe der Info-Schaltfläche', + 'es-es': 'Color del botón de información', + 'fr-fr': "Couleur du bouton d'information", + 'ru-ru': 'Цвет кнопки информации', + 'uk-ua': 'Колір інформаційної кнопки', + 'pt-br': 'Cor do botão de informações', + }, + warningButtonColor: { + 'en-us': 'Warning button color', + 'de-ch': 'Farbe der Warnschaltfläche', + 'es-es': 'Color del botón de advertencia', + 'fr-fr': "Couleur du bouton d'avertissement", + 'ru-ru': 'Цвет кнопки предупреждения', + 'uk-ua': 'Колір кнопки попередження', + 'pt-br': 'Cor do botão de aviso', + }, + successButtonColor: { + 'en-us': 'Success button color', + 'de-ch': 'Farbe der Schaltfläche „Erfolg“', + 'es-es': 'Color del botón de éxito', + 'fr-fr': 'Couleur du bouton de réussite', + 'ru-ru': 'Цвет кнопки «Успех»', + 'uk-ua': 'Колір кнопки успіху', + 'pt-br': 'Cor do botão de sucesso', + }, + openAsReadOnly: { + 'en-us': 'Open all records in read-only mode', + 'de-ch': 'Alle Datensätze im schreibgeschützten Modus öffnen', + 'es-es': 'Abrir todos los registros en modo de solo lectura', + 'fr-fr': 'Ouvrir tous les enregistrements en mode lecture seule', + 'ru-ru': 'Открыть все записи в режиме только для чтения', + 'uk-ua': 'Відкрити всі записи в режимі лише для читання', + 'pt-br': 'Abra todos os registros no modo somente leitura', + }, + displayBasicView: { + 'en-us': 'Display basic view', + 'de-ch': 'Basisansicht anzeigen', + 'es-es': 'Mostrar vista básica', + 'fr-fr': 'Afficher la vue de base', + 'ru-ru': 'Отобразить базовый вид', + 'uk-ua': 'Відобразити базовий вигляд', + 'pt-br': 'Exibir visualização básica', + }, + showComparisonOperatorsForString: { + 'en-us': 'Show comparison operators for text-based fields', + 'de-ch': 'Vergleichsoperatoren für textbasierte Felder anzeigen', + 'es-es': 'Mostrar operadores de comparación para campos basados en texto', + 'fr-fr': 'Afficher les opérateurs de comparaison pour les champs textuels', + 'pt-br': 'Mostrar operadores de comparação para campos baseados em texto', + 'ru-ru': 'Показать операторы сравнения для текстовых полей', + 'uk-ua': 'Показати оператори порівняння для текстових полів', + }, + showComparisonOperatorsDescription: { + 'en-us': + 'Allows the following filters to apply to text fields: Greater Than, Less Than, Greater Than or Equal to, and Less Than or Equal to', + 'de-ch': + 'Ermöglicht die Anwendung der folgenden Filter auf Textfelder: Größer als, Kleiner als, Größer als oder gleich und Kleiner als oder gleich', + 'es-es': + 'Permite aplicar los siguientes filtros a los campos de texto: Mayor que, Menor que, Mayor o igual que y Menor o igual que', + 'fr-fr': + "Permet d'appliquer les filtres suivants aux champs de texte : Supérieur à, Inférieur à, Supérieur ou égal à et Inférieur ou égal à", + 'pt-br': + 'Permite que os seguintes filtros sejam aplicados aos campos de texto: Maior que, Menor que, Maior ou igual a e Menor ou igual a', + 'ru-ru': + 'Позволяет применять к текстовым полям следующие фильтры: «Больше», «Меньше», «Больше или равно» и «Меньше или равно».', + 'uk-ua': + 'Дозволяє застосовувати до текстових полів такі фільтри: «Більше ніж», «Менше ніж», «Більше або дорівнює» та «Менше або дорівнює»', + }, + basicView: { + 'en-us': 'Basic view', + 'de-ch': 'Basisansicht', + 'es-es': 'Vista básica', + 'fr-fr': 'Vue de base', + 'ru-ru': 'Базовый вид', + 'uk-ua': 'Основний вигляд', + 'pt-br': 'Visão básica', + }, + detailedView: { + 'en-us': 'Detailed view', + 'de-ch': 'Detailansicht', + 'es-es': 'Vista detallada', + 'fr-fr': 'Vue détaillée', + 'ru-ru': 'Подробный вид', + 'uk-ua': 'Детальний вигляд', + 'pt-br': 'Visão detalhada', + }, + attachmentPreviewMode: { + 'en-us': 'Attachment preview mode', + 'de-ch': 'Anhangsvorschaumodus', + 'es-es': 'Modo de vista previa de archivos adjuntos', + 'fr-fr': "Mode d'aperçu des pièces jointes", + 'ru-ru': 'Режим предварительного просмотра вложений', + 'uk-ua': 'Режим попереднього перегляду вкладених файлів', + 'pt-br': 'Modo de visualização de anexos', + }, + fullResolution: { + 'en-us': 'Full Resolution', + 'de-ch': 'Volle Auflösung', + 'es-es': 'Resolución completa', + 'fr-fr': 'Pleine résolution', + 'ru-ru': 'Полное разрешение', + 'uk-ua': 'Повна роздільна здатність', + 'pt-br': 'Resolução completa', + }, + thumbnail: { + 'en-us': 'Thumbnail', + 'de-ch': 'Miniaturansicht', + 'es-es': 'Uña del pulgar', + 'fr-fr': 'Vignette', + 'ru-ru': 'Миниатюра', + 'uk-ua': 'Мініатюра', + 'pt-br': 'Miniatura', + }, + addSearchBarHomePage: { + 'en-us': 'Add Search Bar on home page', + 'de-ch': 'Suchleiste auf der Startseite hinzufügen', + 'es-es': 'Agregar barra de búsqueda en la página de inicio', + 'fr-fr': "Ajouter une barre de recherche sur la page d'accueil", + 'ru-ru': 'Добавить панель поиска на домашнюю страницу', + 'uk-ua': 'Додайте рядок пошуку на головну сторінку', + 'pt-br': 'Adicionar barra de pesquisa na página inicial', + }, + inheritanceCatNumberPref: { + 'en-us': + 'Enable the inheritance of the primary catalog number to its empty siblings.', + 'de-ch': + 'Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.', + 'es-es': + 'Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.', + 'fr-fr': + "Activer l'héritage du numéro de catalogue principal à ses frères vides.", + 'pt-br': + 'Habilitar a herança do número de catálogo primário para seus irmãos vazios.', + 'ru-ru': + 'Включить наследование основного каталожного номера его пустыми родственными номерами.', + 'uk-ua': + 'Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.', + }, + inheritanceCatNumberParentCOPref: { + 'en-us': + 'Enable the inheritance of the parent catalog number to its empty children.', + 'de-ch': + 'Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.', + 'es-es': + 'Habilitar la herencia del número de catálogo padre a sus hijos vacíos.', + 'fr-fr': + "Activer l'héritage du numéro de catalogue parent à ses enfants vides.", + 'pt-br': + 'Habilita a herança do número do catálogo pai para seus filhos vazios.', + 'ru-ru': + 'Включить наследование родительского каталожного номера его пустыми дочерними элементами.', + 'uk-ua': + 'Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.', + }, + uniqueCatNumberAcrossCompAndCo: { + 'en-us': + 'Catalog Number field need to be unique across Component and CO tables', + 'de-ch': + 'Das Feld „Katalognummer“ muss in allen Komponenten- und CO-Tabellen eindeutig sein', + 'es-es': + 'El campo Número de catálogo debe ser único en las tablas de componentes y CO', + 'fr-fr': + 'Le champ Numéro de catalogue doit être unique dans les tables Composant et CO', + 'pt-br': + 'O campo Número de catálogo precisa ser exclusivo nas tabelas Componente e CO', + 'ru-ru': + 'Поле «Номер каталога» должно быть уникальным в таблицах «Компонент» и «CO».', + 'uk-ua': + 'Поле «Номер у каталозі» має бути унікальним у таблицях «Компонент» та «CO».', + }, +} as const; + +export default preferencesBehaviorDict; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts new file mode 100644 index 00000000000..aa09742e5ff --- /dev/null +++ b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts @@ -0,0 +1,700 @@ +/** + * Localization strings for content and asset preferences. + * + * @module + */ + +export const preferencesContentDict = { + content: { + 'en-us': 'Content', + 'ru-ru': 'Содержание', + 'es-es': 'Contenido', + 'fr-fr': 'Contenu', + 'uk-ua': 'Зміст', + 'de-ch': 'Inhalt', + 'pt-br': 'Contente', + }, + defaultImage: { + 'en-us': 'Specify Logo', + 'ru-ru': 'Укажите логотип', + 'es-es': 'Especificar logotipo', + 'fr-fr': 'Spécifier le logo', + 'uk-ua': 'Вкажіть логотип', + 'de-ch': 'Logo angeben', + 'pt-br': 'Especificar logotipo', + }, + customImage: { + 'en-us': 'Custom Image', + 'ru-ru': 'Пользовательское изображение', + 'es-es': 'Imagen personalizada', + 'fr-fr': 'Image personnalisée', + 'uk-ua': 'Спеціальне зображення', + 'de-ch': 'Benutzerdefiniertes Bild', + 'pt-br': 'Imagem personalizada', + }, + embeddedWebpage: { + 'en-us': 'Embedded web page', + 'ru-ru': 'Встроенная веб-страница', + 'es-es': 'Página web incrustada', + 'fr-fr': 'Page Web intégrée', + 'uk-ua': 'Вбудована веб-сторінка', + 'de-ch': 'Eingebettete Webseite', + 'pt-br': 'Página da web incorporada', + }, + embeddedWebpageDescription: { + 'en-us': 'A URL to a page that would be embedded on the home page:', + 'ru-ru': 'URL-адрес страницы, которая будет встроена в домашнюю страницу:', + 'es-es': 'Una URL a una página que se integrará en la página de inicio:', + 'fr-fr': "Une URL vers une page qui serait intégrée à la page d'accueil :", + 'uk-ua': 'URL-адреса сторінки, яка буде вбудована на домашній сторінці:', + 'de-ch': + 'Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:', + 'pt-br': 'Um URL para uma página que seria incorporada na página inicial:', + }, + behavior: { + 'en-us': 'Behavior', + 'ru-ru': 'Поведение', + 'es-es': 'Comportamiento', + 'fr-fr': 'Comportement', + 'uk-ua': 'Поведінка', + 'de-ch': 'Verhalten', + 'pt-br': 'Comportamento', + }, + noRestrictionsMode: { + 'en-us': 'No restrictions mode', + 'ru-ru': 'Режим без ограничений', + 'es-es': 'Modo sin restricciones', + 'fr-fr': 'Mode sans restriction', + 'uk-ua': 'Режим без обмежень', + 'de-ch': 'Modus „Keine Einschränkungen“', + 'pt-br': 'Modo sem restrições', + }, + noRestrictionsModeWbDescription: { + 'en-us': 'Allows uploading data to any field in any table.', + 'ru-ru': 'Позволяет загружать данные в любое поле любой таблицы.', + 'es-es': 'Permite cargar datos a cualquier campo de cualquier tabla.', + 'fr-fr': + "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", + 'uk-ua': 'Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.', + 'pt-br': 'Permite carregar dados em qualquer campo de qualquer tabela.', + }, + noRestrictionsModeQueryDescription: { + 'en-us': 'Allows querying data from any field in any table.', + 'ru-ru': 'Позволяет запрашивать данные из любого поля любой таблицы.', + 'es-es': 'Permite consultar datos de cualquier campo de cualquier tabla.', + 'fr-fr': + "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", + 'uk-ua': 'Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.', + 'pt-br': 'Permite consultar dados de qualquer campo em qualquer tabela.', + }, + noRestrictionsModeWarning: { + 'en-us': + 'WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.', + 'ru-ru': + 'ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.', + 'es-es': + 'ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.', + 'uk-ua': + 'ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.', + 'de-ch': + 'WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.', + 'fr-fr': + "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", + 'pt-br': + 'AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.', + }, + adminsOnlyPreference: { + 'en-us': "You don't have permission to change this option", + 'ru-ru': 'У вас нет разрешения на изменение этой опции.', + 'es-es': 'No tienes permiso para cambiar esta opción', + 'fr-fr': "Vous n'êtes pas autorisé à modifier cette option", + 'uk-ua': 'Ви не маєте дозволу змінювати цей параметр', + 'de-ch': 'Sie haben keine Berechtigung, diese Option zu ändern', + 'pt-br': 'Você não tem permissão para alterar esta opção', + }, + stickyScrolling: { + 'en-us': 'Sticky scroll bar', + 'ru-ru': 'Липкая полоса прокрутки', + 'es-es': 'Barra de desplazamiento fija', + 'fr-fr': 'Barre de défilement collante', + 'uk-ua': 'Липка смуга прокрутки', + 'de-ch': 'Klebrige Bildlaufleiste', + 'pt-br': 'Barra de rolagem fixa', + }, + foreground: { + 'en-us': 'Foreground', + 'ru-ru': 'Передний план', + 'es-es': 'Primer plano', + 'fr-fr': 'Premier plan', + 'uk-ua': 'Передній план', + 'de-ch': 'Vordergrund', + 'pt-br': 'Primeiro plano', + }, + background: { + 'en-us': 'Background', + 'ru-ru': 'Фон', + 'es-es': 'Fondo', + 'fr-fr': 'Arrière-plan', + 'uk-ua': 'Фон', + 'de-ch': 'Hintergrund', + 'pt-br': 'Fundo', + }, + sidebarTheme: { + 'en-us': 'Sidebar theme', + 'de-ch': 'Seitenleistenthema', + 'es-es': 'Tema de la barra lateral', + 'fr-fr': 'Thème de la barre latérale', + 'ru-ru': 'Тема боковой панели', + 'uk-ua': 'Тема бічної панелі', + 'pt-br': 'Tema da barra lateral', + }, + darkForeground: { + 'en-us': 'Foreground (dark theme)', + 'ru-ru': 'Передний план (тёмная тема)', + 'es-es': 'Primer plano (tema oscuro)', + 'fr-fr': 'Premier plan (thème sombre)', + 'uk-ua': 'Передній план (темна тема)', + 'de-ch': 'Vordergrund (dunkles Design)', + 'pt-br': 'Primeiro plano (tema escuro)', + }, + darkBackground: { + 'en-us': 'Background (dark theme)', + 'ru-ru': 'Фон (тёмная тема)', + 'es-es': 'Fondo (tema oscuro)', + 'fr-fr': 'Arrière-plan (thème sombre)', + 'uk-ua': 'Фон (темна тема)', + 'de-ch': 'Hintergrund (dunkles Design)', + 'pt-br': 'Plano de fundo (tema escuro)', + }, + accentColor1: { + 'en-us': 'Accent color 1', + 'ru-ru': 'Акцентный цвет 1', + 'es-es': 'Color de acento 1', + 'fr-fr': "Couleur d'accent 1", + 'uk-ua': 'Акцентний колір 1', + 'de-ch': 'Akzentfarbe 1', + 'pt-br': 'Cor de destaque 1', + }, + accentColor2: { + 'en-us': 'Accent color 2', + 'ru-ru': 'Акцентный цвет 2', + 'es-es': 'Color de acento 2', + 'fr-fr': "Couleur d'accent 2", + 'uk-ua': 'Акцентний колір 2', + 'de-ch': 'Akzentfarbe 2', + 'pt-br': 'Cor de destaque 2', + }, + accentColor3: { + 'en-us': 'Accent color 3', + 'ru-ru': 'Акцентный цвет 3', + 'es-es': 'Color de acento 3', + 'fr-fr': "Couleur d'accent 3", + 'uk-ua': 'Акцентний колір 3', + 'de-ch': 'Akzentfarbe 3', + 'pt-br': 'Cor de destaque 3', + }, + accentColor4: { + 'en-us': 'Accent color 4', + 'ru-ru': 'Акцентный цвет 4', + 'es-es': 'Color de acento 4', + 'fr-fr': "Couleur d'accent 4", + 'uk-ua': 'Акцентний колір 4', + 'de-ch': 'Akzentfarbe 4', + 'pt-br': 'Cor de destaque 4', + }, + accentColor5: { + 'en-us': 'Accent color 5', + 'ru-ru': 'Акцентный цвет 5', + 'es-es': 'Color de acento 5', + 'fr-fr': "Couleur d'accent 5", + 'uk-ua': 'Акцентний колір 5', + 'de-ch': 'Akzentfarbe 5', + 'pt-br': 'Cor de destaque 5', + }, + spreadsheet: { + 'en-us': 'Spreadsheet', + 'ru-ru': 'Электронная таблица', + 'es-es': 'Hoja de cálculo', + 'fr-fr': 'Tableur', + 'uk-ua': 'Електронна таблиця', + 'de-ch': 'Kalkulationstabelle', + 'pt-br': 'Planilha', + }, + minSpareRows: { + 'en-us': 'Number of blank rows at the end', + 'ru-ru': 'Количество пустых строк в конце', + 'es-es': 'Número de filas en blanco al final', + 'fr-fr': 'Nombre de lignes vides à la fin', + 'uk-ua': 'Кількість порожніх рядків у кінці', + 'de-ch': 'Anzahl der leeren Zeilen am Ende', + 'pt-br': 'Número de linhas em branco no final', + }, + autoWrapCols: { + 'en-us': 'Navigate to the other side when reaching the edge column', + 'ru-ru': 'Достигнув крайней колонны, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la columna del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la colonne de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете краю колонки', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a coluna da borda', + }, + autoWrapRows: { + 'en-us': 'Navigate to the other side when reaching the edge row', + 'ru-ru': 'Достигнув крайнего ряда, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la fila del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la rangée de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете крайнього ряду', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a fileira de bordas', + }, + enterBeginsEditing: { + 'en-us': 'Enter key begins editing cell', + 'ru-ru': 'Клавиша Enter начинает редактирование ячейки.', + 'es-es': 'La tecla Enter inicia la edición de la celda', + 'fr-fr': 'La touche Entrée commence à modifier la cellule', + 'uk-ua': 'Клавіша Enter починає редагування клітинки', + 'de-ch': 'Mit der Eingabetaste beginnt die Bearbeitung der Zelle', + 'pt-br': 'A tecla Enter inicia a edição da célula', + }, + tabMoveDirection: { + 'en-us': 'Direction of movement when Tab key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Tab', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Tab', + 'fr-fr': + 'Sens de déplacement lorsque la touche Tabulation est enfoncée', + 'uk-ua': 'Напрямок руху при натисканні клавіші Tab', + 'de-ch': 'Bewegungsrichtung beim Drücken der Tab-Taste', + 'pt-br': 'Direção do movimento quando a tecla Tab é pressionada', + }, + tabMoveDirectionDescription: { + 'en-us': + 'You can move in the opposite direction by pressing Shift+Tab.', + 'ru-ru': + 'Вы можете двигаться в обратном направлении, нажав Shift+Tab.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Tab.', + 'fr-fr': + 'Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.', + 'uk-ua': + 'Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Tab.', + }, + column: { + 'en-us': 'Column', + 'ru-ru': 'Столбец', + 'es-es': 'Columna', + 'fr-fr': 'Colonne', + 'uk-ua': 'Колонка', + 'de-ch': 'Spalte', + 'pt-br': 'Coluna', + }, + row: { + 'en-us': 'Row', + 'ru-ru': 'Ряд', + 'es-es': 'Fila', + 'fr-fr': 'Rangée', + 'uk-ua': 'рядок', + 'de-ch': 'Reihe', + 'pt-br': 'Linha', + }, + enterMoveDirection: { + 'en-us': 'Direction of movement when Enter key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Enter', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Enter', + 'uk-ua': 'Напрямок руху, коли натиснуто клавішу Enter', + 'de-ch': 'Bewegungsrichtung beim Drücken der Taste Enter', + 'fr-fr': + 'Direction du mouvement lorsque la touche Entrer est enfoncée', + 'pt-br': + 'Direção do movimento quando a tecla Enter é pressionada', + }, + enterMoveDirectionDescription: { + 'en-us': + 'You can move in the opposite direction by pressing Shift+Enter.', + 'ru-ru': + 'Вы можете двигаться в противоположном направлении, нажав Shift+Enter.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Enter.', + 'fr-fr': 'Synonyme couleur.', + 'uk-ua': + 'Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Enter.', + }, + filterPickLists: { + 'en-us': 'Filter pick list items', + 'ru-ru': 'Фильтрация элементов списка выбора', + 'es-es': 'Filtrar elementos de la lista de selección', + 'fr-fr': 'Filtrer les éléments de la liste de sélection', + 'uk-ua': 'Фільтр вибору елементів списку', + 'de-ch': 'Auswahllistenelemente filtern', + 'pt-br': 'Filtrar itens da lista de seleção', + }, + exportFileDelimiter: { + 'en-us': 'Export file delimiter', + 'ru-ru': 'Разделитель файлов экспорта', + 'es-es': 'Delimitador de archivo de exportación', + 'fr-fr': "Délimiteur de fichier d'exportation", + 'uk-ua': 'Роздільник файлу експорту', + 'de-ch': 'Dateitrennzeichen exportieren', + 'pt-br': 'Delimitador de arquivo de exportação', + }, + exportCsvUtf8Bom: { + 'en-us': 'Add UTF-8 BOM to CSV file exports', + 'ru-ru': 'Добавить UTF-8 BOM в экспорт CSV-файла', + 'es-es': 'Agregar BOM UTF-8 a las exportaciones de archivos CSV', + 'fr-fr': 'Ajouter UTF-8 BOM aux exportations de fichiers CSV', + 'uk-ua': 'Додайте специфікацію UTF-8 до експорту файлу CSVу', + 'de-ch': 'UTF-8 BOM zum CSV-Dateiexport hinzufügen', + 'pt-br': 'Adicionar UTF-8 BOM às exportações de arquivos CSV', + }, + exportCsvUtf8BomDescription: { + 'en-us': + 'Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.', + 'ru-ru': 'Корректное отображение экспортированных CSV-файлов в Excel.', + 'es-es': + 'Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.', + 'fr-fr': + "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", + 'uk-ua': 'Змушує експорт файлів CSV правильно відображатися в Excel.', + 'de-ch': + 'Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.', + 'pt-br': + 'Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.', + }, + caseSensitive: { + 'en-us': 'Case-sensitive', + 'ru-ru': 'С учетом регистра', + 'es-es': 'Distingue mayúsculas y minúsculas', + 'fr-fr': 'Sensible aux majuscules et minuscules', + 'uk-ua': 'Чутливий до регістру', + 'de-ch': 'Groß- und Kleinschreibung beachten', + 'pt-br': 'Maiúsculas e minúsculas', + }, + caseInsensitive: { + 'en-us': 'Case-insensitive', + 'ru-ru': 'Без учета регистра', + 'es-es': 'No distingue entre mayúsculas y minúsculas', + 'fr-fr': 'Insensible à la casse', + 'uk-ua': 'Регістр не враховується', + 'de-ch': 'Groß- und Kleinschreibung wird nicht berücksichtigt', + 'pt-br': 'Não diferencia maiúsculas de minúsculas', + }, + showNoReadTables: { + 'en-us': 'Show tables without "Read" access', + 'ru-ru': 'Показывать таблицы без доступа «Чтение»', + 'es-es': 'Mostrar tablas sin acceso de "Lectura"', + 'fr-fr': 'Afficher les tableaux sans accès "Lecture"', + 'uk-ua': 'Показувати таблиці без доступу «Читання»', + 'de-ch': 'Tabellen ohne Lesezugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso de "Leitura"', + }, + showNoAccessTables: { + 'en-us': 'Show tables without "Create" access', + 'ru-ru': 'Показывать таблицы без права «Создать»', + 'es-es': 'Mostrar tablas sin acceso "Crear"', + 'fr-fr': 'Afficher les tableaux sans accès "Créer"', + 'uk-ua': 'Показувати таблиці без доступу «Створити»', + 'de-ch': 'Tabellen ohne „Erstellen“-Zugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso "Criar"', + }, + textAreaAutoGrow: { + 'en-us': 'Text boxes grow automatically', + 'ru-ru': 'Текстовые поля увеличиваются автоматически', + 'es-es': 'Los cuadros de texto crecen automáticamente', + 'fr-fr': "Les zones de texte s'agrandissent automatiquement", + 'uk-ua': 'Текстові поля збільшуються автоматично', + 'de-ch': 'Textfelder werden automatisch vergrößert', + 'pt-br': 'As caixas de texto crescem automaticamente', + }, + clearQueryFilters: { + 'en-us': 'Reset query filters', + 'ru-ru': 'Сбросить фильтры запроса', + 'es-es': 'Restablecer filtros de consulta', + 'fr-fr': 'Réinitialiser les filtres de requête', + 'uk-ua': 'Скинути фільтри запитів', + 'de-ch': 'Abfragefilter zurücksetzen', + 'pt-br': 'Redefinir filtros de consulta', + }, + clearQueryFiltersDescription: { + 'en-us': 'Clears all query filters when running a Report from a Form.', + 'de-ch': + 'Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.', + 'es-es': + 'Borra todos los filtros de consulta al ejecutar un informe desde un formulario.', + 'fr-fr': + "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", + 'ru-ru': 'Очищает все фильтры запроса при запуске отчета из формы.', + 'uk-ua': 'Очищає всі фільтри запитів під час запуску звіту з форми.', + 'pt-br': + 'Limpa todos os filtros de consulta ao executar um relatório de um formulário.', + }, + queryParamtersFromForm: { + 'en-us': 'Show query filters when running a Report from a Form', + 'de-ch': + 'Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird', + 'es-es': + 'Mostrar filtros de consulta al ejecutar un informe desde un formulario', + 'fr-fr': + "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", + 'ru-ru': 'Показывать фильтры запроса при запуске отчета из формы', + 'uk-ua': 'Показувати фільтри запитів під час запуску звіту з форми', + 'pt-br': + 'Mostrar filtros de consulta ao executar um relatório de um formulário', + }, + autoGrowAutoComplete: { + 'en-us': 'Allow autocomplete to grow as wide as need', + 'ru-ru': + 'Разрешить автозаполнению расширяться настолько, насколько это необходимо', + 'es-es': 'Permitir que el autocompletado crezca tanto como sea necesario', + 'fr-fr': + 'Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée', + 'uk-ua': + 'Дозволити автозаповнення розширюватися настільки, наскільки потрібно', + 'de-ch': + 'Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen', + 'pt-br': + 'Permitir que o preenchimento automático cresça o quanto for necessário', + }, + tableNameInTitle: { + 'en-us': 'Include table name in the browser page title', + 'ru-ru': 'Включить имя таблицы в заголовок страницы браузера', + 'es-es': + 'Incluir el nombre de la tabla en el título de la página del navegador', + 'fr-fr': + 'Inclure le nom de la table dans le titre de la page du navigateur', + 'uk-ua': 'Включіть назву таблиці в заголовок сторінки браузера', + 'de-ch': 'Tabellennamen in den Seitentitel des Browsers aufnehmen', + 'pt-br': 'Incluir nome da tabela no título da página do navegador', + }, + focusFirstField: { + 'en-us': 'Focus first field', + 'de-ch': 'Fokus erstes Feld', + 'es-es': 'Enfoque el primer campo', + 'fr-fr': 'Concentrez-vous sur le premier champ', + 'ru-ru': 'Фокус первого поля', + 'uk-ua': 'Перейти до першого поля', + 'pt-br': 'Foco primeiro campo', + }, + doubleClickZoom: { + 'en-us': 'Double click to zoom', + 'ru-ru': 'Дважды щелкните, чтобы увеличить', + 'es-es': 'Haga doble clic para ampliar', + 'fr-fr': 'Double-cliquez pour zoomer', + 'uk-ua': 'Двічі клацніть, щоб збільшити', + 'de-ch': 'Zum Vergrößern doppelklicken', + 'pt-br': 'Clique duas vezes para ampliar', + }, + closePopupOnClick: { + 'en-us': 'Close pop-up on outside click', + 'ru-ru': 'Закрытие всплывающего окна при внешнем щелчке', + 'es-es': 'Cerrar ventana emergente al hacer clic desde fuera', + 'fr-fr': "Fermer la pop-up lors d'un clic extérieur", + 'uk-ua': 'Закрити спливаюче вікно при зовнішньому клацанні', + 'de-ch': 'Popup bei externem Klick schließen', + 'pt-br': 'Fechar pop-up ao clicar fora', + }, + animateTransitions: { + 'en-us': 'Animate transitions', + 'ru-ru': 'Анимированные переходы', + 'es-es': 'Animar transiciones', + 'fr-fr': 'Animer les transitions', + 'uk-ua': 'Анімація переходів', + 'de-ch': 'Übergänge animieren', + 'pt-br': 'Transições animadas', + }, + panInertia: { + 'en-us': 'Pan inertia', + 'ru-ru': 'Инерция пан', + 'es-es': 'Inercia de la sartén', + 'fr-fr': 'Inertie du bac', + 'uk-ua': 'Інерція панорами', + 'de-ch': 'Schwenkträgheit', + 'pt-br': 'Inércia da panela', + }, + mouseDrags: { + 'en-us': 'Mouse drags', + 'ru-ru': 'Перетаскивание мышью', + 'es-es': 'El ratón arrastra', + 'uk-ua': 'Виділіть відповідний підрядок', + 'de-ch': 'Maus zieht', + 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', + 'pt-br': 'Arrastos do mouse', + }, + scrollWheelZoom: { + 'en-us': 'Scroll wheel zoom', + 'ru-ru': 'Масштабирование с помощью колеса прокрутки', + 'es-es': 'Zoom con rueda de desplazamiento', + 'fr-fr': 'Zoom avec la molette de défilement', + 'uk-ua': 'Масштаб колеса прокрутки', + 'de-ch': 'Scrollrad-Zoom', + 'pt-br': 'Zoom da roda de rolagem', + }, + flexibleColumnWidth: { + 'en-us': 'Flexible column width', + 'ru-ru': 'Гибкая ширина столбца', + 'es-es': 'Ancho de columna flexible', + 'fr-fr': 'Largeur de colonne flexible', + 'uk-ua': 'Гнучка ширина колонки', + 'de-ch': 'Flexible Spaltenbreite', + 'pt-br': 'Largura de coluna flexível', + }, + flexibleSubGridColumnWidth: { + 'en-us': 'Flexible subview grid column width', + 'ru-ru': 'Гибкая ширина столбца сетки подпредставлений', + 'es-es': 'Ancho de columna de cuadrícula de subvista flexible', + 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', + 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', + 'de-ch': 'Flexible Rasterspaltenbreite der Unteransicht', + 'pt-br': 'Largura flexível da coluna da grade de subvisualização', + }, + closeOnEsc: { + 'en-us': 'Close on ESC key press', + 'ru-ru': 'Закрыть нажатием клавиши ESC', + 'es-es': 'Cerrar al presionar la tecla ESC', + 'fr-fr': 'Icône et nom de la table', + 'uk-ua': 'Закриття натисканням клавіші ESC', + 'de-ch': 'Schließen durch Drücken der Taste ESC', + 'pt-br': 'Fechar ao pressionar a tecla ESC', + }, + closeOnOutsideClick: { + 'en-us': 'Close on outside click', + 'ru-ru': 'Закрытие по внешнему щелчку', + 'es-es': 'Cerrar al hacer clic desde fuera', + 'fr-fr': 'Fermer sur clic extérieur', + 'uk-ua': 'Закрийте зовнішнім клацанням', + 'de-ch': 'Schließen durch Klicken von außen', + 'pt-br': 'Fechar com clique externo', + }, + scopeEntireTablePicklists: { + 'en-us': 'Scope "Entire Table" picklists', + }, + scopeEntireTablePicklistsDescription: { + 'en-us': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + }, + catalogNumberInheritanceDescription: { + 'en-us': + 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', + }, + catalogNumberParentInheritanceDescription: { + 'en-us': + 'Control whether component records inherit catalog numbers from their parent Collection Object.', + }, + specifyNetworkBadge: { + 'en-us': 'Specify Network Badge', + 'ru-ru': 'Укажите сетевой значок', + 'es-es': 'Especificar la insignia de red', + 'fr-fr': 'Spécifier le badge réseau', + 'uk-ua': 'Укажіть значок мережі', + 'de-ch': 'Netzwerk-Badge angeben', + 'pt-br': 'Especificar emblema de rede', + }, + useAccessibleFullDatePicker: { + 'en-us': 'Use accessible full date picker', + 'ru-ru': 'Используйте доступный полный выбор даты', + 'es-es': 'Utilice el selector de fecha completo y accesible', + 'fr-fr': 'Utiliser un sélecteur de date complet accessible', + 'uk-ua': 'Використовуйте доступний повний засіб вибору дати', + 'de-ch': 'Verwenden Sie eine barrierefreie Datumsauswahl', + 'pt-br': 'Use o seletor de data completo acessível', + }, + useAccessibleMonthPicker: { + 'en-us': 'Use accessible month picker', + 'ru-ru': 'Используйте доступный выбор месяца', + 'es-es': 'Utilice el selector de meses accesible', + 'fr-fr': 'Utiliser le sélecteur de mois accessible', + 'uk-ua': 'Використовуйте доступний засіб вибору місяця', + 'de-ch': 'Verwenden Sie die barrierefreie Monatsauswahl', + 'pt-br': 'Use o seletor de meses acessível', + }, + rightAlignNumberFields: { + 'en-us': 'Right-Justify numeric fields', + 'ru-ru': 'Выравнивание числовых полей по правому краю', + 'es-es': 'Justificar a la derecha los campos numéricos', + 'fr-fr': 'Justifier à droite les champs numériques', + 'uk-ua': 'Вирівнювання по правому краю числових полів', + 'de-ch': 'Rechtsbündige Ausrichtung numerischer Felder', + 'pt-br': 'Justificar à direita campos numéricos', + }, + roundedCorners: { + 'en-us': 'Rounded corners', + 'ru-ru': 'Закругленные углы', + 'es-es': 'esquinas redondeadas', + 'fr-fr': 'Coins arrondis', + 'uk-ua': 'Заокруглені кути', + 'de-ch': 'Abgerundete Ecken', + 'pt-br': 'Cantos arredondados', + }, + showSubviewBorders: { + 'en-us': 'Show borders around subviews', + 'de-ch': 'Rahmen um Unteransichten anzeigen', + 'es-es': 'Mostrar bordes alrededor de las subvistas', + 'fr-fr': 'Afficher les bordures autour des sous-vues', + 'pt-br': 'Mostrar bordas ao redor das subvisualizações', + 'ru-ru': 'Показывать границы вокруг подпредставлений', + 'uk-ua': 'Показати межі навколо підвидів', + }, + limitMaxFieldWidth: { + 'en-us': 'Limit max field width', + 'ru-ru': 'Ограничить максимальную ширину поля', + 'es-es': 'Limitar el ancho máximo del campo', + 'fr-fr': 'Limiter la largeur maximale du champ', + 'uk-ua': 'Обмеження максимальної ширини поля', + 'de-ch': 'Maximale Feldbreite begrenzen', + 'pt-br': 'Limite a largura máxima do campo', + }, + condenseQueryResults: { + 'en-us': 'Condense query results', + 'ru-ru': 'Сжать результаты запроса', + 'es-es': 'Condensar los resultados de la consulta', + 'fr-fr': 'Condenser les résultats de la requête', + 'uk-ua': 'Згорнути результати запиту', + 'de-ch': 'Abfrageergebnisse verdichten', + 'pt-br': 'Condensar resultados da consulta', + }, + blurContentBehindDialog: { + 'en-us': 'Blur content behind the dialog', + 'ru-ru': 'Размытие содержимого за диалогом', + 'es-es': 'Desenfocar el contenido detrás del diálogo', + 'fr-fr': 'Flou le contenu derrière la boîte de dialogue', + 'uk-ua': 'Розмити вміст за діалоговим вікном', + 'de-ch': 'Inhalte hinter dem Dialog verwischen', + 'pt-br': 'Desfocar o conteúdo atrás do diálogo', + }, + collectionSortOrderDescription: { + 'en-us': 'This determines the visual order of collections.', + 'ru-ru': 'Это определяет визуальный порядок коллекций.', + 'es-es': 'Esto determina el orden visual de las colecciones.', + 'fr-fr': "Ceci détermine l'ordre visuel des collections.", + 'uk-ua': 'Це визначає візуальний порядок колекцій.', + 'de-ch': 'Dies bestimmt die visuelle Reihenfolge der Sammlungen.', + 'pt-br': 'Isso determina a ordem visual das coleções.', + }, + recordSetRecordToOpen: { + 'en-us': 'Record to open by default', + 'ru-ru': 'Запись для открытия по умолчанию', + 'es-es': 'Registro para abrir por defecto', + 'fr-fr': 'Enregistrement à ouvrir par défaut', + 'uk-ua': 'Запис відкривається за умовчанням', + 'de-ch': 'Standardmäßig zu öffnender Datensatz', + 'pt-br': 'Gravar para abrir por padrão', + }, +} as const; + +export default preferencesContentDict; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index fc7c813b9a0..544708ca970 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1,14 +1,13 @@ /** - * Localization strings for the preferences menu + * Localization strings for the preferences menu (split into manageable sections). * * @module */ - import { createDictionary } from './utils'; - +import { preferencesContentDict } from './preferences.content'; +import { preferencesBehaviorDict } from './preferences.behavior'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file - -export const preferencesText = createDictionary({ +export const preferencesGeneralDict= { preferences: { 'en-us': 'Preferences', 'ru-ru': 'Настройки', @@ -706,1416 +705,14 @@ export const preferencesText = createDictionary({ 'uk-ua': 'Домашня сторінка', 'de-ch': 'Startseite', 'pt-br': 'Página inicial', - }, - content: { - 'en-us': 'Content', - 'ru-ru': 'Содержание', - 'es-es': 'Contenido', - 'fr-fr': 'Contenu', - 'uk-ua': 'Зміст', - 'de-ch': 'Inhalt', - 'pt-br': 'Contente', - }, - defaultImage: { - 'en-us': 'Specify Logo', - 'ru-ru': 'Укажите логотип', - 'es-es': 'Especificar logotipo', - 'fr-fr': 'Spécifier le logo', - 'uk-ua': 'Вкажіть логотип', - 'de-ch': 'Logo angeben', - 'pt-br': 'Especificar logotipo', - }, - customImage: { - 'en-us': 'Custom Image', - 'ru-ru': 'Пользовательское изображение', - 'es-es': 'Imagen personalizada', - 'fr-fr': 'Image personnalisée', - 'uk-ua': 'Спеціальне зображення', - 'de-ch': 'Benutzerdefiniertes Bild', - 'pt-br': 'Imagem personalizada', - }, - embeddedWebpage: { - 'en-us': 'Embedded web page', - 'ru-ru': 'Встроенная веб-страница', - 'es-es': 'Página web incrustada', - 'fr-fr': 'Page Web intégrée', - 'uk-ua': 'Вбудована веб-сторінка', - 'de-ch': 'Eingebettete Webseite', - 'pt-br': 'Página da web incorporada', - }, - embeddedWebpageDescription: { - 'en-us': 'A URL to a page that would be embedded on the home page:', - 'ru-ru': 'URL-адрес страницы, которая будет встроена в домашнюю страницу:', - 'es-es': 'Una URL a una página que se integrará en la página de inicio:', - 'fr-fr': "Une URL vers une page qui serait intégrée à la page d'accueil :", - 'uk-ua': 'URL-адреса сторінки, яка буде вбудована на домашній сторінці:', - 'de-ch': - 'Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:', - 'pt-br': 'Um URL para uma página que seria incorporada na página inicial:', - }, - behavior: { - 'en-us': 'Behavior', - 'ru-ru': 'Поведение', - 'es-es': 'Comportamiento', - 'fr-fr': 'Comportement', - 'uk-ua': 'Поведінка', - 'de-ch': 'Verhalten', - 'pt-br': 'Comportamento', - }, - noRestrictionsMode: { - 'en-us': 'No restrictions mode', - 'ru-ru': 'Режим без ограничений', - 'es-es': 'Modo sin restricciones', - 'fr-fr': 'Mode sans restriction', - 'uk-ua': 'Режим без обмежень', - 'de-ch': 'Modus „Keine Einschränkungen“', - 'pt-br': 'Modo sem restrições', - }, - noRestrictionsModeWbDescription: { - 'en-us': 'Allows uploading data to any field in any table.', - 'ru-ru': 'Позволяет загружать данные в любое поле любой таблицы.', - 'es-es': 'Permite cargar datos a cualquier campo de cualquier tabla.', - 'fr-fr': - "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", - 'uk-ua': 'Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.', - 'de-ch': - 'Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.', - 'pt-br': 'Permite carregar dados em qualquer campo de qualquer tabela.', - }, - noRestrictionsModeQueryDescription: { - 'en-us': 'Allows querying data from any field in any table.', - 'ru-ru': 'Позволяет запрашивать данные из любого поля любой таблицы.', - 'es-es': 'Permite consultar datos de cualquier campo de cualquier tabla.', - 'fr-fr': - "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", - 'uk-ua': 'Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.', - 'de-ch': - 'Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.', - 'pt-br': 'Permite consultar dados de qualquer campo em qualquer tabela.', - }, - noRestrictionsModeWarning: { - 'en-us': - 'WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.', - 'ru-ru': - 'ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.', - 'es-es': - 'ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.', - 'uk-ua': - 'ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.', - 'de-ch': - 'WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.', - 'fr-fr': - "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", - 'pt-br': - 'AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.', - }, - adminsOnlyPreference: { - 'en-us': "You don't have permission to change this option", - 'ru-ru': 'У вас нет разрешения на изменение этой опции.', - 'es-es': 'No tienes permiso para cambiar esta opción', - 'fr-fr': "Vous n'êtes pas autorisé à modifier cette option", - 'uk-ua': 'Ви не маєте дозволу змінювати цей параметр', - 'de-ch': 'Sie haben keine Berechtigung, diese Option zu ändern', - 'pt-br': 'Você não tem permissão para alterar esta opção', - }, - stickyScrolling: { - 'en-us': 'Sticky scroll bar', - 'ru-ru': 'Липкая полоса прокрутки', - 'es-es': 'Barra de desplazamiento fija', - 'fr-fr': 'Barre de défilement collante', - 'uk-ua': 'Липка смуга прокрутки', - 'de-ch': 'Klebrige Bildlaufleiste', - 'pt-br': 'Barra de rolagem fixa', - }, - foreground: { - 'en-us': 'Foreground', - 'ru-ru': 'Передний план', - 'es-es': 'Primer plano', - 'fr-fr': 'Premier plan', - 'uk-ua': 'Передній план', - 'de-ch': 'Vordergrund', - 'pt-br': 'Primeiro plano', - }, - background: { - 'en-us': 'Background', - 'ru-ru': 'Фон', - 'es-es': 'Fondo', - 'fr-fr': 'Arrière-plan', - 'uk-ua': 'Фон', - 'de-ch': 'Hintergrund', - 'pt-br': 'Fundo', - }, - sidebarTheme: { - 'en-us': 'Sidebar theme', - 'de-ch': 'Seitenleistenthema', - 'es-es': 'Tema de la barra lateral', - 'fr-fr': 'Thème de la barre latérale', - 'ru-ru': 'Тема боковой панели', - 'uk-ua': 'Тема бічної панелі', - 'pt-br': 'Tema da barra lateral', - }, - darkForeground: { - 'en-us': 'Foreground (dark theme)', - 'ru-ru': 'Передний план (тёмная тема)', - 'es-es': 'Primer plano (tema oscuro)', - 'fr-fr': 'Premier plan (thème sombre)', - 'uk-ua': 'Передній план (темна тема)', - 'de-ch': 'Vordergrund (dunkles Design)', - 'pt-br': 'Primeiro plano (tema escuro)', - }, - darkBackground: { - 'en-us': 'Background (dark theme)', - 'ru-ru': 'Фон (тёмная тема)', - 'es-es': 'Fondo (tema oscuro)', - 'fr-fr': 'Arrière-plan (thème sombre)', - 'uk-ua': 'Фон (темна тема)', - 'de-ch': 'Hintergrund (dunkles Design)', - 'pt-br': 'Plano de fundo (tema escuro)', - }, - accentColor1: { - 'en-us': 'Accent color 1', - 'ru-ru': 'Акцентный цвет 1', - 'es-es': 'Color de acento 1', - 'fr-fr': "Couleur d'accent 1", - 'uk-ua': 'Акцентний колір 1', - 'de-ch': 'Akzentfarbe 1', - 'pt-br': 'Cor de destaque 1', - }, - accentColor2: { - 'en-us': 'Accent color 2', - 'ru-ru': 'Акцентный цвет 2', - 'es-es': 'Color de acento 2', - 'fr-fr': "Couleur d'accent 2", - 'uk-ua': 'Акцентний колір 2', - 'de-ch': 'Akzentfarbe 2', - 'pt-br': 'Cor de destaque 2', - }, - accentColor3: { - 'en-us': 'Accent color 3', - 'ru-ru': 'Акцентный цвет 3', - 'es-es': 'Color de acento 3', - 'fr-fr': "Couleur d'accent 3", - 'uk-ua': 'Акцентний колір 3', - 'de-ch': 'Akzentfarbe 3', - 'pt-br': 'Cor de destaque 3', - }, - accentColor4: { - 'en-us': 'Accent color 4', - 'ru-ru': 'Акцентный цвет 4', - 'es-es': 'Color de acento 4', - 'fr-fr': "Couleur d'accent 4", - 'uk-ua': 'Акцентний колір 4', - 'de-ch': 'Akzentfarbe 4', - 'pt-br': 'Cor de destaque 4', - }, - accentColor5: { - 'en-us': 'Accent color 5', - 'ru-ru': 'Акцентный цвет 5', - 'es-es': 'Color de acento 5', - 'fr-fr': "Couleur d'accent 5", - 'uk-ua': 'Акцентний колір 5', - 'de-ch': 'Akzentfarbe 5', - 'pt-br': 'Cor de destaque 5', - }, - spreadsheet: { - 'en-us': 'Spreadsheet', - 'ru-ru': 'Электронная таблица', - 'es-es': 'Hoja de cálculo', - 'fr-fr': 'Tableur', - 'uk-ua': 'Електронна таблиця', - 'de-ch': 'Kalkulationstabelle', - 'pt-br': 'Planilha', - }, - minSpareRows: { - 'en-us': 'Number of blank rows at the end', - 'ru-ru': 'Количество пустых строк в конце', - 'es-es': 'Número de filas en blanco al final', - 'fr-fr': 'Nombre de lignes vides à la fin', - 'uk-ua': 'Кількість порожніх рядків у кінці', - 'de-ch': 'Anzahl der leeren Zeilen am Ende', - 'pt-br': 'Número de linhas em branco no final', - }, - autoWrapCols: { - 'en-us': 'Navigate to the other side when reaching the edge column', - 'ru-ru': 'Достигнув крайней колонны, перейдите на другую сторону.', - 'es-es': 'Navegue hacia el otro lado al llegar a la columna del borde.', - 'fr-fr': - 'Naviguez de l’autre côté lorsque vous atteignez la colonne de bord', - 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете краю колонки', - 'de-ch': - 'Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen', - 'pt-br': 'Navegue para o outro lado ao atingir a coluna da borda', - }, - autoWrapRows: { - 'en-us': 'Navigate to the other side when reaching the edge row', - 'ru-ru': 'Достигнув крайнего ряда, перейдите на другую сторону.', - 'es-es': 'Navegue hacia el otro lado al llegar a la fila del borde.', - 'fr-fr': - 'Naviguez de l’autre côté lorsque vous atteignez la rangée de bord', - 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете крайнього ряду', - 'de-ch': - 'Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen', - 'pt-br': 'Navegue para o outro lado ao atingir a fileira de bordas', - }, - enterBeginsEditing: { - 'en-us': 'Enter key begins editing cell', - 'ru-ru': 'Клавиша Enter начинает редактирование ячейки.', - 'es-es': 'La tecla Enter inicia la edición de la celda', - 'fr-fr': 'La touche Entrée commence à modifier la cellule', - 'uk-ua': 'Клавіша Enter починає редагування клітинки', - 'de-ch': 'Mit der Eingabetaste beginnt die Bearbeitung der Zelle', - 'pt-br': 'A tecla Enter inicia a edição da célula', - }, - tabMoveDirection: { - 'en-us': 'Direction of movement when Tab key is pressed', - 'ru-ru': 'Направление движения при нажатии клавиши Tab', - 'es-es': - 'Dirección de movimiento cuando se presiona la tecla Tab', - 'fr-fr': - 'Sens de déplacement lorsque la touche Tabulation est enfoncée', - 'uk-ua': 'Напрямок руху при натисканні клавіші Tab', - 'de-ch': 'Bewegungsrichtung beim Drücken der Tab-Taste', - 'pt-br': 'Direção do movimento quando a tecla Tab é pressionada', - }, - tabMoveDirectionDescription: { - 'en-us': - 'You can move in the opposite direction by pressing Shift+Tab.', - 'ru-ru': - 'Вы можете двигаться в обратном направлении, нажав Shift+Tab.', - 'es-es': - 'Puedes moverte en la dirección opuesta presionando Shift+Tab.', - 'fr-fr': - 'Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.', - 'uk-ua': - 'Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.', - 'de-ch': - 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.', - 'pt-br': - 'Você pode mover na direção oposta pressionando Shift+Tab.', - }, - column: { - 'en-us': 'Column', - 'ru-ru': 'Столбец', - 'es-es': 'Columna', - 'fr-fr': 'Colonne', - 'uk-ua': 'Колонка', - 'de-ch': 'Spalte', - 'pt-br': 'Coluna', - }, - row: { - 'en-us': 'Row', - 'ru-ru': 'Ряд', - 'es-es': 'Fila', - 'fr-fr': 'Rangée', - 'uk-ua': 'рядок', - 'de-ch': 'Reihe', - 'pt-br': 'Linha', - }, - enterMoveDirection: { - 'en-us': 'Direction of movement when Enter key is pressed', - 'ru-ru': 'Направление движения при нажатии клавиши Enter', - 'es-es': - 'Dirección de movimiento cuando se presiona la tecla Enter', - 'uk-ua': 'Напрямок руху, коли натиснуто клавішу Enter', - 'de-ch': 'Bewegungsrichtung beim Drücken der Taste Enter', - 'fr-fr': - 'Direction du mouvement lorsque la touche Entrer est enfoncée', - 'pt-br': - 'Direção do movimento quando a tecla Enter é pressionada', - }, - enterMoveDirectionDescription: { - 'en-us': - 'You can move in the opposite direction by pressing Shift+Enter.', - 'ru-ru': - 'Вы можете двигаться в противоположном направлении, нажав Shift+Enter.', - 'es-es': - 'Puedes moverte en la dirección opuesta presionando Shift+Enter.', - 'fr-fr': 'Synonyme couleur.', - 'uk-ua': - 'Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.', - 'de-ch': - 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.', - 'pt-br': - 'Você pode mover na direção oposta pressionando Shift+Enter.', - }, - filterPickLists: { - 'en-us': 'Filter pick list items', - 'ru-ru': 'Фильтрация элементов списка выбора', - 'es-es': 'Filtrar elementos de la lista de selección', - 'fr-fr': 'Filtrer les éléments de la liste de sélection', - 'uk-ua': 'Фільтр вибору елементів списку', - 'de-ch': 'Auswahllistenelemente filtern', - 'pt-br': 'Filtrar itens da lista de seleção', - }, - exportFileDelimiter: { - 'en-us': 'Export file delimiter', - 'ru-ru': 'Разделитель файлов экспорта', - 'es-es': 'Delimitador de archivo de exportación', - 'fr-fr': "Délimiteur de fichier d'exportation", - 'uk-ua': 'Роздільник файлу експорту', - 'de-ch': 'Dateitrennzeichen exportieren', - 'pt-br': 'Delimitador de arquivo de exportação', - }, - exportCsvUtf8Bom: { - 'en-us': 'Add UTF-8 BOM to CSV file exports', - 'ru-ru': 'Добавить UTF-8 BOM в экспорт CSV-файла', - 'es-es': 'Agregar BOM UTF-8 a las exportaciones de archivos CSV', - 'fr-fr': 'Ajouter UTF-8 BOM aux exportations de fichiers CSV', - 'uk-ua': 'Додайте специфікацію UTF-8 до експорту файлу CSVу', - 'de-ch': 'UTF-8 BOM zum CSV-Dateiexport hinzufügen', - 'pt-br': 'Adicionar UTF-8 BOM às exportações de arquivos CSV', - }, - exportCsvUtf8BomDescription: { - 'en-us': - 'Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.', - 'ru-ru': 'Корректное отображение экспортированных CSV-файлов в Excel.', - 'es-es': - 'Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.', - 'fr-fr': - "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", - 'uk-ua': 'Змушує експорт файлів CSV правильно відображатися в Excel.', - 'de-ch': - 'Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.', - 'pt-br': - 'Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.', - }, - caseSensitive: { - 'en-us': 'Case-sensitive', - 'ru-ru': 'С учетом регистра', - 'es-es': 'Distingue mayúsculas y minúsculas', - 'fr-fr': 'Sensible aux majuscules et minuscules', - 'uk-ua': 'Чутливий до регістру', - 'de-ch': 'Groß- und Kleinschreibung beachten', - 'pt-br': 'Maiúsculas e minúsculas', - }, - caseInsensitive: { - 'en-us': 'Case-insensitive', - 'ru-ru': 'Без учета регистра', - 'es-es': 'No distingue entre mayúsculas y minúsculas', - 'fr-fr': 'Insensible à la casse', - 'uk-ua': 'Регістр не враховується', - 'de-ch': 'Groß- und Kleinschreibung wird nicht berücksichtigt', - 'pt-br': 'Não diferencia maiúsculas de minúsculas', - }, - showNoReadTables: { - 'en-us': 'Show tables without "Read" access', - 'ru-ru': 'Показывать таблицы без доступа «Чтение»', - 'es-es': 'Mostrar tablas sin acceso de "Lectura"', - 'fr-fr': 'Afficher les tableaux sans accès "Lecture"', - 'uk-ua': 'Показувати таблиці без доступу «Читання»', - 'de-ch': 'Tabellen ohne Lesezugriff anzeigen', - 'pt-br': 'Mostrar tabelas sem acesso de "Leitura"', - }, - showNoAccessTables: { - 'en-us': 'Show tables without "Create" access', - 'ru-ru': 'Показывать таблицы без права «Создать»', - 'es-es': 'Mostrar tablas sin acceso "Crear"', - 'fr-fr': 'Afficher les tableaux sans accès "Créer"', - 'uk-ua': 'Показувати таблиці без доступу «Створити»', - 'de-ch': 'Tabellen ohne „Erstellen“-Zugriff anzeigen', - 'pt-br': 'Mostrar tabelas sem acesso "Criar"', - }, - textAreaAutoGrow: { - 'en-us': 'Text boxes grow automatically', - 'ru-ru': 'Текстовые поля увеличиваются автоматически', - 'es-es': 'Los cuadros de texto crecen automáticamente', - 'fr-fr': "Les zones de texte s'agrandissent automatiquement", - 'uk-ua': 'Текстові поля збільшуються автоматично', - 'de-ch': 'Textfelder werden automatisch vergrößert', - 'pt-br': 'As caixas de texto crescem automaticamente', - }, - clearQueryFilters: { - 'en-us': 'Reset query filters', - 'ru-ru': 'Сбросить фильтры запроса', - 'es-es': 'Restablecer filtros de consulta', - 'fr-fr': 'Réinitialiser les filtres de requête', - 'uk-ua': 'Скинути фільтри запитів', - 'de-ch': 'Abfragefilter zurücksetzen', - 'pt-br': 'Redefinir filtros de consulta', - }, - clearQueryFiltersDescription: { - 'en-us': 'Clears all query filters when running a Report from a Form.', - 'de-ch': - 'Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.', - 'es-es': - 'Borra todos los filtros de consulta al ejecutar un informe desde un formulario.', - 'fr-fr': - "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", - 'ru-ru': 'Очищает все фильтры запроса при запуске отчета из формы.', - 'uk-ua': 'Очищає всі фільтри запитів під час запуску звіту з форми.', - 'pt-br': - 'Limpa todos os filtros de consulta ao executar um relatório de um formulário.', - }, - queryParamtersFromForm: { - 'en-us': 'Show query filters when running a Report from a Form', - 'de-ch': - 'Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird', - 'es-es': - 'Mostrar filtros de consulta al ejecutar un informe desde un formulario', - 'fr-fr': - "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", - 'ru-ru': 'Показывать фильтры запроса при запуске отчета из формы', - 'uk-ua': 'Показувати фільтри запитів під час запуску звіту з форми', - 'pt-br': - 'Mostrar filtros de consulta ao executar um relatório de um formulário', - }, - autoGrowAutoComplete: { - 'en-us': 'Allow autocomplete to grow as wide as need', - 'ru-ru': - 'Разрешить автозаполнению расширяться настолько, насколько это необходимо', - 'es-es': 'Permitir que el autocompletado crezca tanto como sea necesario', - 'fr-fr': - 'Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée', - 'uk-ua': - 'Дозволити автозаповнення розширюватися настільки, наскільки потрібно', - 'de-ch': - 'Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen', - 'pt-br': - 'Permitir que o preenchimento automático cresça o quanto for necessário', - }, - tableNameInTitle: { - 'en-us': 'Include table name in the browser page title', - 'ru-ru': 'Включить имя таблицы в заголовок страницы браузера', - 'es-es': - 'Incluir el nombre de la tabla en el título de la página del navegador', - 'fr-fr': - 'Inclure le nom de la table dans le titre de la page du navigateur', - 'uk-ua': 'Включіть назву таблиці в заголовок сторінки браузера', - 'de-ch': 'Tabellennamen in den Seitentitel des Browsers aufnehmen', - 'pt-br': 'Incluir nome da tabela no título da página do navegador', - }, - focusFirstField: { - 'en-us': 'Focus first field', - 'de-ch': 'Fokus erstes Feld', - 'es-es': 'Enfoque el primer campo', - 'fr-fr': 'Concentrez-vous sur le premier champ', - 'ru-ru': 'Фокус первого поля', - 'uk-ua': 'Перейти до першого поля', - 'pt-br': 'Foco primeiro campo', - }, - doubleClickZoom: { - 'en-us': 'Double click to zoom', - 'ru-ru': 'Дважды щелкните, чтобы увеличить', - 'es-es': 'Haga doble clic para ampliar', - 'fr-fr': 'Double-cliquez pour zoomer', - 'uk-ua': 'Двічі клацніть, щоб збільшити', - 'de-ch': 'Zum Vergrößern doppelklicken', - 'pt-br': 'Clique duas vezes para ampliar', - }, - closePopupOnClick: { - 'en-us': 'Close pop-up on outside click', - 'ru-ru': 'Закрытие всплывающего окна при внешнем щелчке', - 'es-es': 'Cerrar ventana emergente al hacer clic desde fuera', - 'fr-fr': "Fermer la pop-up lors d'un clic extérieur", - 'uk-ua': 'Закрити спливаюче вікно при зовнішньому клацанні', - 'de-ch': 'Popup bei externem Klick schließen', - 'pt-br': 'Fechar pop-up ao clicar fora', - }, - animateTransitions: { - 'en-us': 'Animate transitions', - 'ru-ru': 'Анимированные переходы', - 'es-es': 'Animar transiciones', - 'fr-fr': 'Animer les transitions', - 'uk-ua': 'Анімація переходів', - 'de-ch': 'Übergänge animieren', - 'pt-br': 'Transições animadas', - }, - panInertia: { - 'en-us': 'Pan inertia', - 'ru-ru': 'Инерция пан', - 'es-es': 'Inercia de la sartén', - 'fr-fr': 'Inertie du bac', - 'uk-ua': 'Інерція панорами', - 'de-ch': 'Schwenkträgheit', - 'pt-br': 'Inércia da panela', - }, - mouseDrags: { - 'en-us': 'Mouse drags', - 'ru-ru': 'Перетаскивание мышью', - 'es-es': 'El ratón arrastra', - 'uk-ua': 'Виділіть відповідний підрядок', - 'de-ch': 'Maus zieht', - 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', - 'pt-br': 'Arrastos do mouse', - }, - scrollWheelZoom: { - 'en-us': 'Scroll wheel zoom', - 'ru-ru': 'Масштабирование с помощью колеса прокрутки', - 'es-es': 'Zoom con rueda de desplazamiento', - 'fr-fr': 'Zoom avec la molette de défilement', - 'uk-ua': 'Масштаб колеса прокрутки', - 'de-ch': 'Scrollrad-Zoom', - 'pt-br': 'Zoom da roda de rolagem', - }, - flexibleColumnWidth: { - 'en-us': 'Flexible column width', - 'ru-ru': 'Гибкая ширина столбца', - 'es-es': 'Ancho de columna flexible', - 'fr-fr': 'Largeur de colonne flexible', - 'uk-ua': 'Гнучка ширина колонки', - 'de-ch': 'Flexible Spaltenbreite', - 'pt-br': 'Largura de coluna flexível', - }, - flexibleSubGridColumnWidth: { - 'en-us': 'Flexible subview grid column width', - 'ru-ru': 'Гибкая ширина столбца сетки подпредставлений', - 'es-es': 'Ancho de columna de cuadrícula de subvista flexible', - 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', - 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', - 'de-ch': 'Flexible Rasterspaltenbreite der Unteransicht', - 'pt-br': 'Largura flexível da coluna da grade de subvisualização', - }, - closeOnEsc: { - 'en-us': 'Close on ESC key press', - 'ru-ru': 'Закрыть нажатием клавиши ESC', - 'es-es': 'Cerrar al presionar la tecla ESC', - 'fr-fr': 'Icône et nom de la table', - 'uk-ua': 'Закриття натисканням клавіші ESC', - 'de-ch': 'Schließen durch Drücken der Taste ESC', - 'pt-br': 'Fechar ao pressionar a tecla ESC', - }, - closeOnOutsideClick: { - 'en-us': 'Close on outside click', - 'ru-ru': 'Закрытие по внешнему щелчку', - 'es-es': 'Cerrar al hacer clic desde fuera', - 'fr-fr': 'Fermer sur clic extérieur', - 'uk-ua': 'Закрийте зовнішнім клацанням', - 'de-ch': 'Schließen durch Klicken von außen', - 'pt-br': 'Fechar com clique externo', - }, - scopeEntireTablePicklists: { - 'en-us': 'Scope "Entire Table" picklists', - }, - scopeEntireTablePicklistsDescription: { - 'en-us': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - }, - catalogNumberInheritanceDescription: { - 'en-us': - 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', - }, - catalogNumberParentInheritanceDescription: { - 'en-us': - 'Control whether component records inherit catalog numbers from their parent Collection Object.', - }, - specifyNetworkBadge: { - 'en-us': 'Specify Network Badge', - 'ru-ru': 'Укажите сетевой значок', - 'es-es': 'Especificar la insignia de red', - 'fr-fr': 'Spécifier le badge réseau', - 'uk-ua': 'Укажіть значок мережі', - 'de-ch': 'Netzwerk-Badge angeben', - 'pt-br': 'Especificar emblema de rede', - }, - useAccessibleFullDatePicker: { - 'en-us': 'Use accessible full date picker', - 'ru-ru': 'Используйте доступный полный выбор даты', - 'es-es': 'Utilice el selector de fecha completo y accesible', - 'fr-fr': 'Utiliser un sélecteur de date complet accessible', - 'uk-ua': 'Використовуйте доступний повний засіб вибору дати', - 'de-ch': 'Verwenden Sie eine barrierefreie Datumsauswahl', - 'pt-br': 'Use o seletor de data completo acessível', - }, - useAccessibleMonthPicker: { - 'en-us': 'Use accessible month picker', - 'ru-ru': 'Используйте доступный выбор месяца', - 'es-es': 'Utilice el selector de meses accesible', - 'fr-fr': 'Utiliser le sélecteur de mois accessible', - 'uk-ua': 'Використовуйте доступний засіб вибору місяця', - 'de-ch': 'Verwenden Sie die barrierefreie Monatsauswahl', - 'pt-br': 'Use o seletor de meses acessível', - }, - rightAlignNumberFields: { - 'en-us': 'Right-Justify numeric fields', - 'ru-ru': 'Выравнивание числовых полей по правому краю', - 'es-es': 'Justificar a la derecha los campos numéricos', - 'fr-fr': 'Justifier à droite les champs numériques', - 'uk-ua': 'Вирівнювання по правому краю числових полів', - 'de-ch': 'Rechtsbündige Ausrichtung numerischer Felder', - 'pt-br': 'Justificar à direita campos numéricos', - }, - roundedCorners: { - 'en-us': 'Rounded corners', - 'ru-ru': 'Закругленные углы', - 'es-es': 'esquinas redondeadas', - 'fr-fr': 'Coins arrondis', - 'uk-ua': 'Заокруглені кути', - 'de-ch': 'Abgerundete Ecken', - 'pt-br': 'Cantos arredondados', - }, - showSubviewBorders: { - 'en-us': 'Show borders around subviews', - 'de-ch': 'Rahmen um Unteransichten anzeigen', - 'es-es': 'Mostrar bordes alrededor de las subvistas', - 'fr-fr': 'Afficher les bordures autour des sous-vues', - 'pt-br': 'Mostrar bordas ao redor das subvisualizações', - 'ru-ru': 'Показывать границы вокруг подпредставлений', - 'uk-ua': 'Показати межі навколо підвидів', - }, - limitMaxFieldWidth: { - 'en-us': 'Limit max field width', - 'ru-ru': 'Ограничить максимальную ширину поля', - 'es-es': 'Limitar el ancho máximo del campo', - 'fr-fr': 'Limiter la largeur maximale du champ', - 'uk-ua': 'Обмеження максимальної ширини поля', - 'de-ch': 'Maximale Feldbreite begrenzen', - 'pt-br': 'Limite a largura máxima do campo', - }, - condenseQueryResults: { - 'en-us': 'Condense query results', - 'ru-ru': 'Сжать результаты запроса', - 'es-es': 'Condensar los resultados de la consulta', - 'fr-fr': 'Condenser les résultats de la requête', - 'uk-ua': 'Згорнути результати запиту', - 'de-ch': 'Abfrageergebnisse verdichten', - 'pt-br': 'Condensar resultados da consulta', - }, - blurContentBehindDialog: { - 'en-us': 'Blur content behind the dialog', - 'ru-ru': 'Размытие содержимого за диалогом', - 'es-es': 'Desenfocar el contenido detrás del diálogo', - 'fr-fr': 'Flou le contenu derrière la boîte de dialogue', - 'uk-ua': 'Розмити вміст за діалоговим вікном', - 'de-ch': 'Inhalte hinter dem Dialog verwischen', - 'pt-br': 'Desfocar o conteúdo atrás do diálogo', - }, - collectionSortOrderDescription: { - 'en-us': 'This determines the visual order of collections.', - 'ru-ru': 'Это определяет визуальный порядок коллекций.', - 'es-es': 'Esto determina el orden visual de las colecciones.', - 'fr-fr': "Ceci détermine l'ordre visuel des collections.", - 'uk-ua': 'Це визначає візуальний порядок колекцій.', - 'de-ch': 'Dies bestimmt die visuelle Reihenfolge der Sammlungen.', - 'pt-br': 'Isso determina a ordem visual das coleções.', - }, - recordSetRecordToOpen: { - 'en-us': 'Record to open by default', - 'ru-ru': 'Запись для открытия по умолчанию', - 'es-es': 'Registro para abrir por defecto', - 'fr-fr': 'Enregistrement à ouvrir par défaut', - 'uk-ua': 'Запис відкривається за умовчанням', - 'de-ch': 'Standardmäßig zu öffnender Datensatz', - 'pt-br': 'Gravar para abrir por padrão', - }, - altClickToSupressNewTab: { - 'en-us': - '{altKeyName:string}+Click to suppress new tab', - 'ru-ru': - '{altKeyName:string}+Нажмите , чтобы скрыть новую вкладку', - 'es-es': - '{altKeyName:string}+Haga clic en para suprimir la nueva pestaña', - 'fr-fr': - '{altKeyName:string}+Cliquez sur pour supprimer le nouvel onglet', - 'uk-ua': - '{altKeyName:string}+Натисніть , щоб закрити нову вкладку', - 'de-ch': - '{altKeyName:string}+Klicken Sie auf, um neue Registerkarten zu unterdrücken', - 'pt-br': - '{altKeyName:string}+Clique em para suprimir a nova guia', - }, - altClickToSupressNewTabDescription: { - 'en-us': - '{altKeyName:string}+Click a link that usually opens in a new tab to open it in the current tab.', - 'ru-ru': - '{altKeyName:string}+Нажмите на ссылку, которая обычно открывается в новой вкладке, чтобы открыть ее в текущей вкладке.', - 'es-es': - '{altKeyName:string}+Haga clic en un enlace que normalmente se abre en una nueva pestaña para abrirlo en la pestaña actual.', - 'fr-fr': 'Utiliser le sélecteur de mois accessible.', - 'uk-ua': - '{altKeyName:string}+Натисніть посилання, яке зазвичай відкривається в новій вкладці, щоб відкрити його в поточній вкладці.', - 'de-ch': - '{altKeyName:string}+Klicken Sie auf einen Link, der normalerweise in einem neuen Tab geöffnet wird, um ihn im aktuellen Tab zu öffnen.', - 'pt-br': - '{altKeyName:string}+Clique em um link que geralmente abre em uma nova aba para abri-lo na aba atual.', - }, - makeFormDialogsModal: { - 'en-us': 'Make form dialogs gray out the background', - 'ru-ru': 'Сделать фон диалоговых окон серым', - 'es-es': - 'Hacer que los cuadros de diálogo del formulario tengan el fondo en gris', - 'fr-fr': - "Rendre les boîtes de dialogue de formulaire grisées sur l'arrière-plan", - 'uk-ua': 'Зробіть діалогові вікна форми сірими фоном', - 'de-ch': 'Den Hintergrund von Formulardialogen ausgrauen', - 'pt-br': - 'Faça com que as caixas de diálogo do formulário fiquem com o fundo acinzentado', - }, - autoScrollTree: { - 'en-us': 'Auto scroll tree to focused node', - 'ru-ru': 'Автоматическая прокрутка дерева к выбранному узлу', - 'es-es': 'Desplazamiento automático del árbol al nodo enfocado', - 'fr-fr': 'Arbre de défilement automatique vers le nœud ciblé', - 'uk-ua': 'Автоматичне прокручування дерева до виділеного вузла', - 'de-ch': 'Automatisches Scrollen des Baums zum fokussierten Knoten', - 'pt-br': 'Rolagem automática da árvore para o nó em foco', - }, - sortByField: { - 'en-us': 'Order By Field', - 'de-ch': 'Nach Feld sortieren', - 'es-es': 'Ordenar por campo', - 'fr-fr': 'Trier par champ', - 'pt-br': 'Ordenar por campo', - 'ru-ru': 'Сортировать по полю', - 'uk-ua': 'Сортувати за полем', - }, - lineWrap: { - 'en-us': 'Line wrap', - 'ru-ru': 'Перенос строки', - 'es-es': 'Ajuste de línea', - 'fr-fr': 'Retour à la ligne', - 'uk-ua': 'Обтікання лініями', - 'de-ch': 'Zeilenumbruch', - 'pt-br': 'Quebra de linha', - }, - indentSize: { - 'en-us': 'Indent size', - 'ru-ru': 'Размер отступа', - 'es-es': 'Tamaño de sangría', - 'fr-fr': 'Taille du retrait', - 'uk-ua': 'Розмір відступу', - 'de-ch': 'Einzugsgröße', - 'pt-br': 'Tamanho do recuo', - }, - indentWithTab: { - 'en-us': 'Indent with Tab', - 'ru-ru': 'Отступ с помощью Tab', - 'es-es': 'Sangría con Tab', - 'fr-fr': 'Indenter avec Tabulation', - 'uk-ua': 'Відступ із Tab', - 'de-ch': 'Einrücken mit Tab', - 'pt-br': 'Recuo com Tab', - }, - formHeaderFormat: { - 'en-us': 'Form header format', - 'ru-ru': 'Формат заголовка формы', - 'es-es': 'Formato del encabezado del formulario', - 'fr-fr': "Format d'en-tête de formulaire", - 'uk-ua': 'Формат заголовка форми', - 'de-ch': 'Formularkopfformat', - 'pt-br': 'Formato do cabeçalho do formulário', - }, - iconAndTableName: { - 'en-us': 'Icon and table name', - 'ru-ru': 'Значок и название таблицы', - 'es-es': 'Icono y nombre de la tabla', - 'fr-fr': 'Icône et nom de la table', - 'uk-ua': 'Значок і назва таблиці', - 'de-ch': 'Symbol und Tabellenname', - 'pt-br': 'Ícone e nome da tabela', - }, - tableIcon: { - 'en-us': 'Table icon', - 'ru-ru': 'Значок таблицы', - 'es-es': 'Icono de tabla', - 'fr-fr': 'Icône de tableau', - 'uk-ua': 'Значок таблиці', - 'de-ch': 'Tabellensymbol', - 'pt-br': 'Ícone de tabela', - }, - maxHeight: { - 'en-us': 'Max height', - 'ru-ru': 'Максимальная высота', - 'es-es': 'Altura máxima', - 'fr-fr': 'hauteur maximum', - 'uk-ua': 'Максимальна висота', - 'de-ch': 'Maximale Höhe', - 'pt-br': 'Altura máxima', - }, - autoComplete: { - 'en-us': 'Auto complete', - 'ru-ru': 'Автозаполнение', - 'es-es': 'Autocompletar', - 'fr-fr': - "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux", - 'uk-ua': - 'Визначає підписи полів, примітки щодо використання та підписи таблиць', - 'de-ch': 'Autovervollständigung', - 'pt-br': 'Preenchimento automático', - }, - searchCaseSensitive: { - 'en-us': 'Case-sensitive search', - 'es-es': 'Búsqueda que distingue entre mayúsculas y minúsculas', - 'fr-fr': 'Recherche sensible à la casse', - 'uk-ua': 'Пошук з урахуванням регістру', - 'de-ch': 'Groß- und Kleinschreibung beachten', - 'ru-ru': 'Поиск с учетом регистра', - 'pt-br': 'Pesquisa com diferenciação entre maiúsculas e minúsculas', - }, - searchField: { - 'en-us': 'Search Field', - 'ru-ru': 'Поле поиска', - 'es-es': 'Campo de búsqueda', - 'fr-fr': 'Champ de recherche', - 'uk-ua': 'Поле пошуку', - 'de-ch': 'Suchfeld', - 'pt-br': 'Campo de pesquisa', - }, - createInteractions: { - 'en-us': 'Creating an interaction', - 'ru-ru': 'Создание взаимодействия', - 'es-es': 'Creando una interacción', - 'fr-fr': 'Créer une interaction', - 'uk-ua': 'Створення взаємодії', - 'de-ch': 'Erstellen einer Interaktion', - 'pt-br': 'Criando uma interação', - }, - useSpaceAsDelimiter: { - 'en-us': 'Use space as delimiter', - 'ru-ru': 'Используйте пробел в качестве разделителя', - 'es-es': 'Utilice el espacio como delimitador', - 'fr-fr': "Utiliser l'espace comme délimiteur", - 'uk-ua': 'Використовуйте пробіл як роздільник', - 'de-ch': 'Leerzeichen als Trennzeichen verwenden', - 'pt-br': 'Use espaço como delimitador', - }, - useCommaAsDelimiter: { - 'en-us': 'Use comma as delimiter', - 'ru-ru': 'Используйте запятую в качестве разделителя', - 'es-es': 'Utilice la coma como delimitador', - 'fr-fr': 'Utiliser la virgule comme délimiteur', - 'uk-ua': 'Використовуйте кому як роздільник', - 'de-ch': 'Verwenden Sie Kommas als Trennzeichen', - 'pt-br': 'Use vírgula como delimitador', - }, - useNewLineAsDelimiter: { - 'en-us': 'Use new line as delimiter', - 'ru-ru': 'Использовать новую строку в качестве разделителя', - 'es-es': 'Utilice nueva línea como delimitador', - 'fr-fr': 'Utiliser une nouvelle ligne comme délimiteur', - 'uk-ua': 'Використовуйте новий рядок як роздільник', - 'de-ch': 'Neue Zeile als Trennzeichen verwenden', - 'pt-br': 'Use nova linha como delimitador', - }, - useCustomDelimiters: { - 'en-us': 'Use custom delimiters', - 'ru-ru': 'Используйте пользовательские разделители', - 'es-es': 'Utilice delimitadores personalizados', - 'fr-fr': 'Utiliser des délimiteurs personnalisés', - 'uk-ua': 'Використовуйте спеціальні роздільники', - 'de-ch': 'Benutzerdefinierte Trennzeichen verwenden', - 'pt-br': 'Use delimitadores personalizados', - }, - useCustomDelimitersDescription: { - 'en-us': - 'A list of delimiters to use, in addition to the ones defined above. Put one delimiter per line.', - 'ru-ru': - 'Список разделителей, которые можно использовать в дополнение к указанным выше. Используйте по одному разделителю на строку.', - 'es-es': - 'Una lista de delimitadores para usar, además de los definidos anteriormente. Coloque un delimitador por línea.', - 'fr-fr': - 'Une liste de délimiteurs à utiliser, en plus de ceux définis ci-dessus. Mettez un délimiteur par ligne.', - 'uk-ua': - 'Список розділювачів для використання на додаток до визначених вище. Поставте один роздільник на рядок.', - 'de-ch': - 'Eine Liste der zu verwendenden Trennzeichen zusätzlich zu den oben definierten. Geben Sie pro Zeile ein Trennzeichen ein.', - 'pt-br': - 'Uma lista de delimitadores a serem usados, além dos definidos acima. Coloque um delimitador por linha.', - }, - detectAutomaticallyDescription: { - 'en-us': 'Detect automatically based on catalog number format.', - 'ru-ru': 'Автоматическое определение на основе формата каталожного номера.', - 'es-es': - 'Detectar automáticamente según el formato del número de catálogo.', - 'fr-fr': - 'Détecter automatiquement en fonction du format du numéro de catalogue.', - 'uk-ua': 'Визначати автоматично на основі формату номера каталогу.', - 'de-ch': 'Automatische Erkennung basierend auf dem Katalognummernformat.', - 'pt-br': - 'Detectar automaticamente com base no formato do número de catálogo.', - }, - use: { - comment: 'Verb', - 'en-us': 'Use', - 'ru-ru': 'Использовать', - 'es-es': 'Usar', - 'fr-fr': 'Utiliser', - 'uk-ua': 'використання', - 'de-ch': 'Verwenden', - 'pt-br': 'Usar', - }, - dontUse: { - 'en-us': 'Don’t use', - 'ru-ru': 'Не использовать', - 'es-es': 'No utilizar', - 'fr-fr': 'Zoom avec la molette de défilement', - 'uk-ua': 'Масштаб колеса прокрутки', - 'de-ch': 'Nicht verwenden', - 'pt-br': 'Não use', - }, - position: { - 'en-us': 'Position', - 'es-es': 'Posición', - 'fr-fr': 'Position', - 'ru-ru': 'Позиция', - 'uk-ua': 'Позиція', - 'de-ch': 'Position', - 'pt-br': 'Posição', - }, - top: { - 'en-us': 'Top', - 'es-es': 'Arriba', - 'fr-fr': 'Haut', - 'ru-ru': 'Вершина', - 'uk-ua': 'Топ', - 'de-ch': 'Spitze', - 'pt-br': 'Principal', - }, - bottom: { - 'en-us': 'Bottom', - 'es-es': 'Abajo', - 'ru-ru': 'Нижний', - 'uk-ua': 'Дно', - 'de-ch': 'Unten', - 'fr-fr': 'Bas', - 'pt-br': 'Fundo', - }, - left: { - 'en-us': 'Left', - 'es-es': 'Izquierda', - 'fr-fr': 'Gauche', - 'ru-ru': 'Левый', - 'uk-ua': 'Ліворуч', - 'de-ch': 'Links', - 'pt-br': 'Esquerda', - }, - right: { - 'en-us': 'Right', - 'es-es': 'Bien', - 'fr-fr': 'Droite', - 'ru-ru': 'Верно', - 'uk-ua': 'правильно', - 'de-ch': 'Rechts', - 'pt-br': 'Certo', - }, - showUnsavedIndicator: { - 'en-us': 'Show unsaved changes indicator', - 'ru-ru': 'Показать индикатор несохраненных изменений', - 'es-es': 'Mostrar indicador de cambios no guardados', - 'fr-fr': "Afficher l'indicateur de modifications non enregistrées", - 'uk-ua': 'Показати індикатор незбережених змін', - 'de-ch': 'Indikator für nicht gespeicherte Änderungen anzeigen', - 'pt-br': 'Mostrar indicador de alterações não salvas', - }, - showUnsavedIndicatorDescription: { - 'en-us': - 'Show an "*" in the tab title when there are unsaved changes in the current tab.', - 'es-es': - 'Mostrar un "*" en el título de la pestaña cuando haya cambios sin guardar en la pestaña actual.', - 'fr-fr': - "Afficher un \"*\" dans le titre de l'onglet lorsqu'il y a des modifications non enregistrées dans l'onglet actuel.", - 'ru-ru': - 'Отображать «*» в заголовке вкладки, если на текущей вкладке есть несохраненные изменения.', - 'uk-ua': - 'Показувати «*» у заголовку вкладки, якщо в поточній вкладці є незбережені зміни.', - 'de-ch': - 'Zeigen Sie im Registerkartentitel ein „*“ an, wenn in der aktuellen Registerkarte nicht gespeicherte Änderungen vorhanden sind.', - 'pt-br': - 'Exibir um "*" no título da aba quando houver alterações não salvas na aba atual.', - }, - autoPopulateDescription: { - 'en-us': - 'Auto populate the merged record with values from duplicates when opening the merging dialog.', - 'ru-ru': - 'Автоматически заполнять объединенную запись значениями из дубликатов при открытии диалогового окна слияния.', - 'de-ch': - 'Füllen Sie den zusammengeführten Datensatz beim Öffnen des Zusammenführungsdialogs automatisch mit Werten aus Duplikaten.', - 'es-es': - 'Rellene automáticamente el registro fusionado con valores de duplicados al abrir el cuadro de diálogo de fusión.', - 'fr-fr': - "Remplir automatiquement l'enregistrement fusionné avec les valeurs des doublons lors de l'ouverture de la boîte de dialogue de fusion.", - 'uk-ua': - 'Автоматичне заповнення об’єднаного запису значеннями з дублікатів під час відкриття діалогового вікна об’єднання.', - 'pt-br': - 'Preencha automaticamente o registro mesclado com valores de duplicatas ao abrir a caixa de diálogo de mesclagem.', - }, - autoCreateVariants: { - 'en-us': 'Automatically create {agentVariantTable:string} records', - 'ru-ru': 'Автоматически создавать записи {agentVariantTable:string}', - 'de-ch': '{agentVariantTable:string}-Datensätze automatisch erstellen', - 'es-es': 'Crear automáticamente registros {agentVariantTable:string}', - 'fr-fr': - 'Créer automatiquement des enregistrements {agentVariantTable:string}', - 'uk-ua': 'Автоматично створювати записи {agentVariantTable:string}', - 'pt-br': 'Criar automaticamente registros {agentVariantTable:string}', - }, - autoCreateVariantsDescription: { - 'en-us': - 'When merging agents, automatically create {agentVariantTable:string} records based on the variations of first name/last name.', - 'ru-ru': - 'При объединении агентов автоматически создавать записи {agentVariantTable:string} на основе вариаций имени/фамилии.', - 'de-ch': - 'Beim Zusammenführen von Agenten werden automatisch {agentVariantTable:string}-Datensätze basierend auf den Variationen von Vorname/Nachname erstellt.', - 'es-es': - 'Al fusionar agentes, se crean automáticamente registros {agentVariantTable:string} basados en las variaciones de nombre/apellido.', - 'fr-fr': - "Lors de la fusion d'agents, créez automatiquement des enregistrements {agentVariantTable:string} en fonction des variations du prénom/nom.", - 'uk-ua': - 'Під час об’єднання агентів автоматично створювати записи {agentVariantTable:string} на основі варіацій імені/прізвища.', - 'pt-br': - 'Ao mesclar agentes, crie automaticamente registros {agentVariantTable:string} com base nas variações de nome/sobrenome.', - }, - collectionPreferences: { - 'en-us': 'Collection Preferences', - 'de-ch': 'Sammlungseinstellungen', - 'es-es': 'Preferencias de colección', - 'fr-fr': 'Personnalisation', - 'ru-ru': 'Настройки коллекции', - 'uk-ua': 'Налаштування', - 'pt-br': 'Preferências de coleção', - }, - rememberDialogSizes: { - 'en-us': 'Remember dialog window sizes', - 'ru-ru': 'Запомните размеры диалоговых окон', - 'es-es': 'Recordar los tamaños de las ventanas de diálogo', - 'fr-fr': 'Mémoriser les tailles des fenêtres de dialogue', - 'uk-ua': "Запам'ятайте розміри діалогових вікон", - 'de-ch': 'Dialogfenstergrößen merken', - 'pt-br': 'Lembrar tamanhos de janelas de diálogo', - }, - rememberDialogPositions: { - 'en-us': 'Remember dialog window positions', - 'ru-ru': 'Запомнить позиции диалоговых окон', - 'es-es': 'Recordar las posiciones de las ventanas de diálogo', - 'fr-fr': 'Mémoriser les positions des fenêtres de dialogue', - 'uk-ua': "Запам'ятовуйте положення діалогового вікна", - 'de-ch': 'Dialogfensterpositionen merken', - 'pt-br': 'Lembrar posições da janela de diálogo', - }, - autoPlayMedia: { - 'en-us': 'Automatically play media', - 'ru-ru': 'Автоматически воспроизводить медиа', - 'es-es': 'Reproducir automáticamente medios', - 'fr-fr': 'Lire automatiquement les médias', - 'uk-ua': 'Автоматичне відтворення медіа', - 'de-ch': 'Medien automatisch abspielen', - 'pt-br': 'Reproduzir mídia automaticamente', - }, - useCustomTooltips: { - 'en-us': 'Use modern tooltips', - 'ru-ru': 'Используйте современные подсказки', - 'es-es': 'Utilice información sobre herramientas moderna', - 'fr-fr': 'Utiliser des info-bulles modernes', - 'uk-ua': 'Використовуйте сучасні підказки', - 'de-ch': 'Verwenden Sie moderne Tooltips', - 'pt-br': 'Use dicas de ferramentas modernas', - }, - alwaysUseQueryBuilder: { - 'en-us': 'Always use query builder search inside of search form', - 'de-ch': - 'Verwenden Sie innerhalb des Suchformulars immer die Abfragegeneratorsuche', - 'es-es': - 'Utilice siempre la búsqueda del generador de consultas dentro del formulario de búsqueda', - 'fr-fr': - 'Utilisez toujours la recherche du générateur de requêtes dans le formulaire de recherche', - 'ru-ru': 'Всегда используйте конструктор запросов внутри формы поиска.', - 'uk-ua': 'Завжди використовуйте пошук конструктора запитів у формі пошуку', - 'pt-br': - 'Sempre use a pesquisa do construtor de consultas dentro do formulário de pesquisa', - }, - localizeResourceNames: { - 'en-us': 'Localize the names of recognized app resources', - 'de-ch': 'Lokalisieren Sie die Namen erkannter App-Ressourcen', - 'es-es': - 'Localizar los nombres de los recursos de aplicaciones reconocidos', - 'fr-fr': "Localiser les noms des ressources d'application reconnues", - 'ru-ru': 'Локализуйте названия распознанных ресурсов приложения', - 'uk-ua': 'Локалізувати назви розпізнаних ресурсів програми', - 'pt-br': 'Localize os nomes dos recursos de aplicativos reconhecidos', - }, - splitLongXml: { - 'en-us': 'Split long lines of XML into multiple lines', - 'de-ch': 'Teilen Sie lange XML-Zeilen in mehrere Zeilen auf', - 'es-es': 'Dividir líneas largas de XML en varias líneas', - 'fr-fr': 'Diviser les longues lignes de XML en plusieurs lignes', - 'ru-ru': 'Разделить длинные строки XML на несколько строк', - 'uk-ua': 'Розділіть довгі рядки XML на кілька рядків', - 'pt-br': 'Dividir longas linhas de XML em várias linhas', - }, - url: { - 'en-us': 'URL', - 'de-ch': 'URL', - 'es-es': 'URL', - 'fr-fr': 'URL', - 'uk-ua': 'URL', - 'ru-ru': 'URL', - 'pt-br': 'URL', - }, - pickAttachment: { - 'en-us': 'Pick an attachment', - 'es-es': 'Elige un archivo adjunto', - 'fr-fr': 'Choisissez une pièce jointe', - 'ru-ru': 'Выберите вложение', - 'uk-ua': 'Виберіть вкладення', - 'de-ch': 'Wählen Sie einen Anhang', - 'pt-br': 'Escolha um anexo', - }, - attachmentFailed: { - 'en-us': 'The attachment failed to load.', - 'de-ch': 'Der Anhang konnte nicht geladen werden.', - 'es-es': 'No se pudo cargar el archivo adjunto.', - 'fr-fr': "La pièce jointe n'a pas pu être chargée.", - 'ru-ru': 'Не удалось загрузить вложение.', - 'uk-ua': 'Не вдалося завантажити вкладений файл.', - 'pt-br': 'O anexo não pôde ser carregado.', - }, - pickImage: { - 'en-us': 'Pick an image', - 'de-ch': 'Wählen Sie ein Bild aus', - 'es-es': 'Elige una imagen', - 'fr-fr': 'Choisissez une image', - 'ru-ru': 'Выберите изображение', - 'uk-ua': 'Виберіть зображення', - 'pt-br': 'Escolha uma imagem', - }, - customLogo: { - 'en-us': 'Expanded Image URL', - 'de-ch': 'Erweiterte Bild-URL', - 'es-es': 'URL de imagen expandida', - 'fr-fr': "URL de l'image étendue", - 'ru-ru': 'URL-адрес развернутого изображения', - 'uk-ua': 'Розширена URL-адреса зображення', - 'pt-br': 'URL da imagem expandida', - }, - customLogoCollapsed: { - 'en-us': 'Collapsed Image URL', - 'de-ch': 'URL des minimierten Bildes', - 'es-es': 'URL de imagen contraída', - 'fr-fr': "URL de l'image réduite", - 'ru-ru': 'URL-адрес свернутого изображения', - 'uk-ua': 'URL-адреса згорнутого зображення', - 'pt-br': 'URL da imagem recolhida', - }, - customLogoDescription: { - 'en-us': - 'A URL to an image that would be displayed next to the Specify logo in the navigation menu.', - 'de-ch': - 'Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.', - 'es-es': - 'Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.', - 'fr-fr': - 'Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.', - 'ru-ru': - 'URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.', - 'uk-ua': - 'URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.', - 'pt-br': - 'Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.', - }, - showLineNumber: { - 'en-us': 'Show query result line number', - 'de-ch': 'Zeilennummer des Abfrageergebnisses anzeigen', - 'es-es': 'Mostrar el número de línea del resultado de la consulta', - 'fr-fr': 'Afficher le numéro de ligne du résultat de la requête', - 'ru-ru': 'Показать номер строки результата запроса', - 'uk-ua': 'Показати номер рядка результату запиту', - 'pt-br': 'Mostrar número da linha do resultado da consulta', - }, - saveButtonColor: { - 'en-us': 'Save button color', - 'de-ch': 'Farbe der Schaltfläche „Speichern“', - 'es-es': 'Guardar color del botón', - 'fr-fr': 'Couleur du bouton Enregistrer', - 'ru-ru': 'Сохранить цвет кнопки', - 'uk-ua': 'Зберегти колір кнопки', - 'pt-br': 'Cor do botão Salvar', - }, - secondaryButtonColor: { - 'en-us': 'Secondary button color', - 'es-es': 'Color del botón secundario', - 'fr-fr': 'Couleur du bouton secondaire', - 'ru-ru': 'Цвет вторичной кнопки', - 'uk-ua': 'Колір вторинної кнопки', - 'de-ch': 'Sekundäre Schaltflächenfarbe', - 'pt-br': 'Cor do botão secundário', - }, - secondaryLightButtonColor: { - 'en-us': 'Secondary light button color', - 'de-ch': 'Farbe der sekundären Lichttaste', - 'es-es': 'Color del botón de luz secundaria', - 'fr-fr': 'Couleur du bouton lumineux secondaire', - 'ru-ru': 'Цвет кнопки дополнительного освещения', - 'uk-ua': 'Колір вторинної світлової кнопки', - 'pt-br': 'Cor do botão de luz secundária', - }, - dangerButtonColor: { - 'en-us': 'Danger button color', - 'de-ch': 'Farbe der Gefahrenschaltfläche', - 'es-es': 'Color del botón de peligro', - 'fr-fr': 'Couleur du bouton de danger', - 'ru-ru': 'Цвет кнопки «Опасность»', - 'uk-ua': 'Колір кнопки небезпеки', - 'pt-br': 'Cor do botão de perigo', - }, - infoButtonColor: { - 'en-us': 'Info button color', - 'de-ch': 'Farbe der Info-Schaltfläche', - 'es-es': 'Color del botón de información', - 'fr-fr': "Couleur du bouton d'information", - 'ru-ru': 'Цвет кнопки информации', - 'uk-ua': 'Колір інформаційної кнопки', - 'pt-br': 'Cor do botão de informações', - }, - warningButtonColor: { - 'en-us': 'Warning button color', - 'de-ch': 'Farbe der Warnschaltfläche', - 'es-es': 'Color del botón de advertencia', - 'fr-fr': "Couleur du bouton d'avertissement", - 'ru-ru': 'Цвет кнопки предупреждения', - 'uk-ua': 'Колір кнопки попередження', - 'pt-br': 'Cor do botão de aviso', - }, - successButtonColor: { - 'en-us': 'Success button color', - 'de-ch': 'Farbe der Schaltfläche „Erfolg“', - 'es-es': 'Color del botón de éxito', - 'fr-fr': 'Couleur du bouton de réussite', - 'ru-ru': 'Цвет кнопки «Успех»', - 'uk-ua': 'Колір кнопки успіху', - 'pt-br': 'Cor do botão de sucesso', - }, - openAsReadOnly: { - 'en-us': 'Open all records in read-only mode', - 'de-ch': 'Alle Datensätze im schreibgeschützten Modus öffnen', - 'es-es': 'Abrir todos los registros en modo de solo lectura', - 'fr-fr': 'Ouvrir tous les enregistrements en mode lecture seule', - 'ru-ru': 'Открыть все записи в режиме только для чтения', - 'uk-ua': 'Відкрити всі записи в режимі лише для читання', - 'pt-br': 'Abra todos os registros no modo somente leitura', - }, - displayBasicView: { - 'en-us': 'Display basic view', - 'de-ch': 'Basisansicht anzeigen', - 'es-es': 'Mostrar vista básica', - 'fr-fr': 'Afficher la vue de base', - 'ru-ru': 'Отобразить базовый вид', - 'uk-ua': 'Відобразити базовий вигляд', - 'pt-br': 'Exibir visualização básica', - }, - showComparisonOperatorsForString: { - 'en-us': 'Show comparison operators for text-based fields', - 'de-ch': 'Vergleichsoperatoren für textbasierte Felder anzeigen', - 'es-es': 'Mostrar operadores de comparación para campos basados en texto', - 'fr-fr': 'Afficher les opérateurs de comparaison pour les champs textuels', - 'pt-br': 'Mostrar operadores de comparação para campos baseados em texto', - 'ru-ru': 'Показать операторы сравнения для текстовых полей', - 'uk-ua': 'Показати оператори порівняння для текстових полів', - }, - showComparisonOperatorsDescription: { - 'en-us': - 'Allows the following filters to apply to text fields: Greater Than, Less Than, Greater Than or Equal to, and Less Than or Equal to', - 'de-ch': - 'Ermöglicht die Anwendung der folgenden Filter auf Textfelder: Größer als, Kleiner als, Größer als oder gleich und Kleiner als oder gleich', - 'es-es': - 'Permite aplicar los siguientes filtros a los campos de texto: Mayor que, Menor que, Mayor o igual que y Menor o igual que', - 'fr-fr': - "Permet d'appliquer les filtres suivants aux champs de texte : Supérieur à, Inférieur à, Supérieur ou égal à et Inférieur ou égal à", - 'pt-br': - 'Permite que os seguintes filtros sejam aplicados aos campos de texto: Maior que, Menor que, Maior ou igual a e Menor ou igual a', - 'ru-ru': - 'Позволяет применять к текстовым полям следующие фильтры: «Больше», «Меньше», «Больше или равно» и «Меньше или равно».', - 'uk-ua': - 'Дозволяє застосовувати до текстових полів такі фільтри: «Більше ніж», «Менше ніж», «Більше або дорівнює» та «Менше або дорівнює»', - }, - basicView: { - 'en-us': 'Basic view', - 'de-ch': 'Basisansicht', - 'es-es': 'Vista básica', - 'fr-fr': 'Vue de base', - 'ru-ru': 'Базовый вид', - 'uk-ua': 'Основний вигляд', - 'pt-br': 'Visão básica', - }, - detailedView: { - 'en-us': 'Detailed view', - 'de-ch': 'Detailansicht', - 'es-es': 'Vista detallada', - 'fr-fr': 'Vue détaillée', - 'ru-ru': 'Подробный вид', - 'uk-ua': 'Детальний вигляд', - 'pt-br': 'Visão detalhada', - }, - attachmentPreviewMode: { - 'en-us': 'Attachment preview mode', - 'de-ch': 'Anhangsvorschaumodus', - 'es-es': 'Modo de vista previa de archivos adjuntos', - 'fr-fr': "Mode d'aperçu des pièces jointes", - 'ru-ru': 'Режим предварительного просмотра вложений', - 'uk-ua': 'Режим попереднього перегляду вкладених файлів', - 'pt-br': 'Modo de visualização de anexos', - }, - fullResolution: { - 'en-us': 'Full Resolution', - 'de-ch': 'Volle Auflösung', - 'es-es': 'Resolución completa', - 'fr-fr': 'Pleine résolution', - 'ru-ru': 'Полное разрешение', - 'uk-ua': 'Повна роздільна здатність', - 'pt-br': 'Resolução completa', - }, - thumbnail: { - 'en-us': 'Thumbnail', - 'de-ch': 'Miniaturansicht', - 'es-es': 'Uña del pulgar', - 'fr-fr': 'Vignette', - 'ru-ru': 'Миниатюра', - 'uk-ua': 'Мініатюра', - 'pt-br': 'Miniatura', - }, - addSearchBarHomePage: { - 'en-us': 'Add Search Bar on home page', - 'de-ch': 'Suchleiste auf der Startseite hinzufügen', - 'es-es': 'Agregar barra de búsqueda en la página de inicio', - 'fr-fr': "Ajouter une barre de recherche sur la page d'accueil", - 'ru-ru': 'Добавить панель поиска на домашнюю страницу', - 'uk-ua': 'Додайте рядок пошуку на головну сторінку', - 'pt-br': 'Adicionar barra de pesquisa na página inicial', - }, - inheritanceCatNumberPref: { - 'en-us': - 'Enable the inheritance of the primary catalog number to its empty siblings.', - 'de-ch': - 'Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.', - 'es-es': - 'Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.', - 'fr-fr': - "Activer l'héritage du numéro de catalogue principal à ses frères vides.", - 'pt-br': - 'Habilitar a herança do número de catálogo primário para seus irmãos vazios.', - 'ru-ru': - 'Включить наследование основного каталожного номера его пустыми родственными номерами.', - 'uk-ua': - 'Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.', - }, - inheritanceCatNumberParentCOPref: { - 'en-us': - 'Enable the inheritance of the parent catalog number to its empty children.', - 'de-ch': - 'Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.', - 'es-es': - 'Habilitar la herencia del número de catálogo padre a sus hijos vacíos.', - 'fr-fr': - "Activer l'héritage du numéro de catalogue parent à ses enfants vides.", - 'pt-br': - 'Habilita a herança do número do catálogo pai para seus filhos vazios.', - 'ru-ru': - 'Включить наследование родительского каталожного номера его пустыми дочерними элементами.', - 'uk-ua': - 'Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.', - }, - uniqueCatNumberAcrossCompAndCo: { - 'en-us': - 'Catalog Number field need to be unique across Component and CO tables', - 'de-ch': - 'Das Feld „Katalognummer“ muss in allen Komponenten- und CO-Tabellen eindeutig sein', - 'es-es': - 'El campo Número de catálogo debe ser único en las tablas de componentes y CO', - 'fr-fr': - 'Le champ Numéro de catalogue doit être unique dans les tables Composant et CO', - 'pt-br': - 'O campo Número de catálogo precisa ser exclusivo nas tabelas Componente e CO', - 'ru-ru': - 'Поле «Номер каталога» должно быть уникальным в таблицах «Компонент» и «CO».', - 'uk-ua': - 'Поле «Номер у каталозі» має бути унікальним у таблицях «Компонент» та «CO».', - }, -} as const); + }, + +} as const; + +const aggregatedPreferences = { + ...preferencesGeneralDict, + ...preferencesContentDict, + ...preferencesBehaviorDict, +} as const; + +export const preferencesText = createDictionary(aggregatedPreferences); From 1c7454af4dfc15f484414bc6d2673cfee5cfb85a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 17 Oct 2025 09:21:02 -0400 Subject: [PATCH 059/100] createDictionary export for modularize prefrences --- .../js_src/lib/localization/preferences.behavior.ts | 5 +++-- .../frontend/js_src/lib/localization/preferences.content.ts | 5 +++-- specifyweb/frontend/js_src/lib/localization/preferences.ts | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index beb694d64ec..23b0dc1dcc8 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -3,8 +3,9 @@ * * @module */ +import { createDictionary } from './utils'; -export const preferencesBehaviorDict = { +export const preferencesBehaviorDict = createDictionary ({ altClickToSupressNewTab: { 'en-us': '{altKeyName:string}+Click to suppress new tab', @@ -726,6 +727,6 @@ export const preferencesBehaviorDict = { 'uk-ua': 'Поле «Номер у каталозі» має бути унікальним у таблицях «Компонент» та «CO».', }, -} as const; +} as const); export default preferencesBehaviorDict; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts index aa09742e5ff..60ac80d0ebb 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts @@ -3,8 +3,9 @@ * * @module */ +import { createDictionary } from './utils'; -export const preferencesContentDict = { +export const preferencesContentDict = createDictionary({ content: { 'en-us': 'Content', 'ru-ru': 'Содержание', @@ -695,6 +696,6 @@ export const preferencesContentDict = { 'de-ch': 'Standardmäßig zu öffnender Datensatz', 'pt-br': 'Gravar para abrir por padrão', }, -} as const; +} as const); export default preferencesContentDict; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 544708ca970..5556c57d063 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -7,7 +7,7 @@ import { createDictionary } from './utils'; import { preferencesContentDict } from './preferences.content'; import { preferencesBehaviorDict } from './preferences.behavior'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file -export const preferencesGeneralDict= { +export const preferencesGeneralDict= createDictionary({ preferences: { 'en-us': 'Preferences', 'ru-ru': 'Настройки', @@ -707,7 +707,7 @@ export const preferencesGeneralDict= { 'pt-br': 'Página inicial', }, -} as const; +} as const); const aggregatedPreferences = { ...preferencesGeneralDict, @@ -715,4 +715,4 @@ const aggregatedPreferences = { ...preferencesBehaviorDict, } as const; -export const preferencesText = createDictionary(aggregatedPreferences); +export const preferencesText = (aggregatedPreferences); From ab64e7076a2645da10c7f7d0261c8a8e42edde7f Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 17 Oct 2025 12:21:54 -0400 Subject: [PATCH 060/100] Refactor preference localization dictionaries to expose raw maps --- .../lib/localization/preferences.behavior.ts | 10 +++++-- .../lib/localization/preferences.content.ts | 10 +++++-- .../js_src/lib/localization/preferences.ts | 30 ++++++++++++------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 23b0dc1dcc8..21105babdc3 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -5,7 +5,7 @@ */ import { createDictionary } from './utils'; -export const preferencesBehaviorDict = createDictionary ({ +export const preferencesBehaviorStrings = { altClickToSupressNewTab: { 'en-us': '{altKeyName:string}+Click to suppress new tab', @@ -727,6 +727,10 @@ export const preferencesBehaviorDict = createDictionary ({ 'uk-ua': 'Поле «Номер у каталозі» має бути унікальним у таблицях «Компонент» та «CO».', }, -} as const); +} as const; -export default preferencesBehaviorDict; +export const preferencesBehaviorText = createDictionary( + preferencesBehaviorStrings +); + +export default preferencesBehaviorText; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts index 60ac80d0ebb..22be0918050 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts @@ -5,7 +5,7 @@ */ import { createDictionary } from './utils'; -export const preferencesContentDict = createDictionary({ +export const preferencesContentStrings = { content: { 'en-us': 'Content', 'ru-ru': 'Содержание', @@ -696,6 +696,10 @@ export const preferencesContentDict = createDictionary({ 'de-ch': 'Standardmäßig zu öffnender Datensatz', 'pt-br': 'Gravar para abrir por padrão', }, -} as const); +} as const; -export default preferencesContentDict; +export const preferencesContentText = createDictionary( + preferencesContentStrings +); + +export default preferencesContentText; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 5556c57d063..7ea7903bc8c 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -4,10 +4,14 @@ * @module */ import { createDictionary } from './utils'; -import { preferencesContentDict } from './preferences.content'; -import { preferencesBehaviorDict } from './preferences.behavior'; +import { + preferencesContentText, +} from './preferences.content'; +import { + preferencesBehaviorText, +} from './preferences.behavior'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file -export const preferencesGeneralDict= createDictionary({ +export const preferencesGeneralStrings = { preferences: { 'en-us': 'Preferences', 'ru-ru': 'Настройки', @@ -705,14 +709,18 @@ export const preferencesGeneralDict= createDictionary({ 'uk-ua': 'Домашня сторінка', 'de-ch': 'Startseite', 'pt-br': 'Página inicial', - }, - -} as const); + }, -const aggregatedPreferences = { - ...preferencesGeneralDict, - ...preferencesContentDict, - ...preferencesBehaviorDict, } as const; -export const preferencesText = (aggregatedPreferences); +export const preferencesGeneralDict = createDictionary(preferencesGeneralStrings); + +type PreferencesText = typeof preferencesGeneralDict & + typeof preferencesContentText & + typeof preferencesBehaviorText; + +export const preferencesText = Object.assign( + preferencesGeneralDict, + preferencesContentText, + preferencesBehaviorText +) as PreferencesText; From c6a0b1d60df1153ca23b463c352943027bd20892 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:30:25 -0500 Subject: [PATCH 061/100] fix: links to docs --- .../frontend/js_src/lib/components/Preferences/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index b6973374ae4..02a26d1d4c7 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -72,13 +72,13 @@ const SUBCATEGORY_DOCS_MAP: Record< > = { treeManagement: { synonymized: { - href: 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987/4', + href: 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987', label: headerText.documentation(), }, }, statistics: { appearance: { - href: 'https://discourse.specifysoftware.org/t/specify-7-statistics/1715', + href: 'https://discourse.specifysoftware.org/t/statistics-page/1135', label: headerText.documentation(), }, }, From 574acf8838d42df223f2ed12096809a6bc82c688 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 17 Oct 2025 14:11:30 -0400 Subject: [PATCH 062/100] Modularize preferences localization dictionary --- .../lib/localization/preferences.behavior.ts | 1013 +++++--- .../lib/localization/preferences.content.ts | 767 +----- .../lib/localization/preferences.general.ts | 2092 +++++++++++++++++ .../js_src/lib/localization/preferences.ts | 727 +----- .../lib/localization/utils/scanUsages.ts | 29 +- 5 files changed, 2981 insertions(+), 1647 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/localization/preferences.general.ts diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 21105babdc3..df9608745f5 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -1,12 +1,743 @@ /** - * Localization strings for behavioral and advanced preferences. + * Localization strings for behavioral preferences. * * @module */ import { createDictionary } from './utils'; - -export const preferencesBehaviorStrings = { - altClickToSupressNewTab: { +// Refer to "Guidelines for Programmers" in ./README.md before editing this file +export const preferencesBehaviorDictionary = { + alwaysPrompt: { + 'en-us': 'Always prompt to choose collection', + 'ru-ru': 'Всегда предлагайте выбрать коллекцию', + 'es-es': 'Siempre dispuesto a elegir la colección', + 'fr-fr': 'Toujours invité à choisir la collection', + 'uk-ua': 'Завжди підкажуть вибрати колекцію', + 'de-ch': 'Immer zur Auswahl der Sammlung auffordern', + 'pt-br': 'Sempre pronto para escolher a coleção', + }, + showNewDataSetWarning: { + 'en-us': 'Show new Data Set warning', + 'ru-ru': 'Показать предупреждение о новом наборе данных', + 'es-es': 'Mostrar nueva advertencia de conjunto de datos', + 'fr-fr': "Afficher un nouvel avertissement sur l'ensemble de données", + 'uk-ua': 'Показати попередження про новий набір даних', + 'de-ch': 'Warnung für neuen Datensatz anzeigen', + 'pt-br': 'Mostrar novo aviso de conjunto de dados', + }, + showNewDataSetWarningDescription: { + 'en-us': 'Show an informational message when creating a new Data Set.', + 'ru-ru': + 'Показывать информационное сообщение при создании нового набора данных.', + 'es-es': + 'Mostrar un mensaje informativo al crear un nuevo conjunto de datos.', + 'fr-fr': + "Afficher un message d'information lors de la création d'un nouvel ensemble de données.", + 'uk-ua': + 'Показувати інформаційне повідомлення під час створення нового набору даних.', + 'de-ch': 'Zeige eine Meldung beim erstellen eines neuen Datensatzes an.', + 'pt-br': + 'Exibir uma mensagem informativa ao criar um novo conjunto de dados.', + }, + allowDismissingErrors: { + 'en-us': 'Allow dismissing error messages', + 'ru-ru': 'Разрешить отклонять сообщения об ошибках', + 'es-es': 'Permitir descartar mensajes de error', + 'fr-fr': "Autoriser le rejet des messages d'erreur", + 'uk-ua': 'Дозволити закривати повідомлення про помилки', + 'de-ch': 'Erlaube das Verwerfen von Fehlermeldungen', + 'pt-br': 'Permitir descartar mensagens de erro', + }, + updatePageTitle: { + 'en-us': 'Update page title', + 'ru-ru': 'Обновить заголовок страницы', + 'es-es': 'Actualizar el título de la página', + 'fr-fr': 'Mettre à jour le titre de la page', + 'uk-ua': 'Оновити назву сторінки', + 'de-ch': 'Seitentitel aktualisieren', + 'pt-br': 'Atualizar título da página', + }, + updatePageTitleDescription: { + 'en-us': + "Whether to update the title of the page to match dialog's header.", + 'ru-ru': + 'Обновлять ли заголовок страницы в соответствии с заголовком диалогового окна.', + 'es-es': + 'Si se debe actualizar el título de la página para que coincida con el encabezado del cuadro de diálogo.', + 'fr-fr': + "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'en-tête de la boîte de dialogue.", + 'uk-ua': + 'Чи оновлювати назву сторінки відповідно до заголовка діалогового вікна.', + 'de-ch': + 'Titel der Seite so aktualisieren, dass er mit der Kopfzeile des Dialogs übereinstimmt.', + 'pt-br': + 'Se o título da página deve ser atualizado para corresponder ao cabeçalho da caixa de diálogo.', + }, + updatePageTitleFormDescription: { + 'en-us': 'Whether to update the title of the page to match current record.', + 'ru-ru': + 'Следует ли обновить заголовок страницы в соответствии с текущей записью.', + 'es-es': + 'Si desea actualizar el título de la página para que coincida con el registro actual.', + 'fr-fr': + "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'enregistrement actuel.", + 'uk-ua': 'Чи оновлювати назву сторінки відповідно до поточного запису.', + 'de-ch': + 'Titel der Seite aktualisieren, damit er mit dem aktuellen Datensatz übereinstimmt.', + 'pt-br': + 'Se o título da página deve ser atualizado para corresponder ao registro atual.', + }, + queryComboBox: { + 'en-us': 'Query Combo Box', + 'ru-ru': 'Поле со списком запросов', + 'es-es': 'Cuadro combinado de consulta', + 'uk-ua': 'Поле зі списком запитів', + 'de-ch': 'Abfrage-Kombinationsfeld', + 'fr-fr': 'Zone de liste déroulante de requête', + 'pt-br': 'Caixa de combinação de consulta', + }, + searchAlgorithm: { + 'en-us': 'Search Algorithm', + 'ru-ru': 'Алгоритм поиска', + 'es-es': 'Algoritmo de búsqueda', + 'fr-fr': 'Algorithme de recherche', + 'uk-ua': 'Алгоритм пошуку', + 'de-ch': 'Suchalgorithmus', + 'pt-br': 'Algoritmo de Busca', + }, + treeSearchAlgorithm: { + 'en-us': 'Search Algorithm (for relationships with tree tables)', + 'ru-ru': 'Алгоритм поиска (для связей с древовидными таблицами)', + 'es-es': 'Algoritmo de búsqueda (para relaciones con tablas de árboles)', + 'fr-fr': + 'Algorithme de recherche (pour les relations avec les tables arborescentes)', + 'uk-ua': 'Алгоритм пошуку (для зв’язків із деревоподібними таблицями)', + 'de-ch': 'Suchalgorithmus (für Beziehungen mit Baumtabellen)', + 'pt-br': 'Algoritmo de busca (para relacionamentos com tabelas de árvore)', + }, + startsWithInsensitive: { + 'en-us': 'Starts With (case-insensitive)', + 'ru-ru': 'Начинается с (без учета регистра)', + 'es-es': 'Comienza con (sin distinguir entre mayúsculas y minúsculas)', + 'fr-fr': 'Commence par (insensible à la casse)', + 'uk-ua': 'Починається з (без урахування регістру)', + 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung wird nicht beachtet)', + 'pt-br': 'Começa com (sem distinção entre maiúsculas e minúsculas)', + }, + startsWithDescription: { + 'en-us': 'Search for values that begin with a given query string.', + 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', + 'es-es': + 'Busque valores que comiencen con una cadena de consulta determinada.', + 'fr-fr': + 'Rechercher des valeurs commençant par une chaîne de requête donnée.', + 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', + 'de-ch': + 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', + 'pt-br': + 'Pesquise valores que começam com uma determinada sequência de consulta.', + }, + startsWithCaseSensitive: { + 'en-us': 'Starts With (case-sensitive)', + 'ru-ru': 'Начинается с (с учетом регистра)', + 'es-es': 'Comienza con (sensible a mayúsculas y minúsculas)', + 'fr-fr': 'Commence par (sensible à la casse)', + 'uk-ua': 'Починається з (з урахуванням регістру)', + 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung beachten)', + 'pt-br': 'Começa com (diferencia maiúsculas de minúsculas)', + }, + startsWithCaseSensitiveDescription: { + 'en-us': 'Search for values that begin with a given query string.', + 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', + 'es-es': + 'Busque valores que comiencen con una cadena de consulta determinada.', + 'fr-fr': + 'Recherchez les valeurs qui commencent par une chaîne de requête donnée.', + 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', + 'de-ch': + 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', + 'pt-br': + 'Pesquise valores que começam com uma determinada sequência de consulta.', + }, + containsInsensitive: { + 'en-us': 'Contains (case-insensitive)', + 'ru-ru': 'Содержит (без учета регистра)', + 'es-es': 'Contiene (sin distinguir entre mayúsculas y minúsculas)', + 'fr-fr': 'Contient (insensible à la casse)', + 'uk-ua': 'Містить (незалежно від регістру)', + 'de-ch': 'Enthält (Groß-/Kleinschreibung wird nicht beachtet)', + 'pt-br': 'Contém (sem distinção entre maiúsculas e minúsculas)', + }, + containsCaseSensitive: { + 'en-us': 'Contains (case-sensitive)', + 'ru-ru': 'Содержит (с учетом регистра)', + 'es-es': 'Contiene (sensible a mayúsculas y minúsculas)', + 'fr-fr': 'Contient (sensible à la casse)', + 'uk-ua': 'Містить (з урахуванням регістру)', + 'de-ch': 'Enthält (Groß-/Kleinschreibung beachten)', + 'pt-br': 'Contém (diferencia maiúsculas de minúsculas)', + }, + containsDescription: { + 'en-us': + 'Search for values that contain a given query string (case-insensitive).', + 'ru-ru': + 'Поиск значений, содержащих заданную строку запроса (без учета регистра).', + 'es-es': + 'Busque valores que contengan una cadena de consulta determinada (sin distinguir entre mayúsculas y minúsculas).', + 'uk-ua': + 'Пошук значень, які містять заданий рядок запиту (незалежно від регістру).', + 'de-ch': + 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (ohne Berücksichtigung der Groß-/Kleinschreibung).', + 'fr-fr': + 'Recherchez les valeurs contenant une chaîne de requête donnée (insensible à la casse).', + 'pt-br': + 'Pesquisar valores que contenham uma determinada sequência de consulta (sem distinção de maiúsculas e minúsculas).', + }, + containsCaseSensitiveDescription: { + 'en-us': + 'Search for values that contain a given query string (case-sensitive).', + 'ru-ru': + 'Поиск значений, содержащих заданную строку запроса (с учетом регистра).', + 'es-es': + 'Busque valores que contengan una cadena de consulta determinada (distingue entre mayúsculas y minúsculas).', + 'fr-fr': + 'Recherchez les valeurs contenant une chaîne de requête donnée (sensible à la casse).', + 'uk-ua': + 'Пошук значень, які містять заданий рядок запиту (з урахуванням регістру).', + 'de-ch': + 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (Groß-/Kleinschreibung beachten).', + 'pt-br': + 'Pesquisar valores que contenham uma determinada sequência de consulta (diferencia maiúsculas de minúsculas).', + }, + containsSecondDescription: { + 'en-us': + 'Can use _ to match any single character or % to match any number of characters.', + 'ru-ru': + 'Можно использовать _ для соответствия любому отдельному символу или % для соответствия любому количеству символов.', + 'es-es': + 'Puede utilizar _ para que coincida con cualquier carácter individual o % para que coincida con cualquier número de caracteres.', + 'fr-fr': + "Peut utiliser _ pour correspondre à n'importe quel caractère ou % pour correspondre à n'importe quel nombre de caractères.", + 'uk-ua': + 'Можна використовувати _ для відповідності будь-якому одному символу або % для відповідності будь-якій кількості символів.', + 'de-ch': + 'Sie können _ verwenden, um ein beliebiges einzelnes Zeichen abzugleichen, oder %, um eine beliebige Anzahl von Zeichen abzugleichen.', + 'pt-br': + 'Pode usar _ para corresponder a qualquer caractere único ou % para corresponder a qualquer número de caracteres.', + }, + highlightMatch: { + 'en-us': 'Highlight matched substring', + 'ru-ru': 'Выделить совпавшую подстроку', + 'es-es': 'Resaltar la subcadena coincidente', + 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', + 'uk-ua': 'Виділіть збіг підрядка', + 'de-ch': 'Markieren Sie übereinstimmende Teilzeichenfolgen', + 'pt-br': 'Destacar substring correspondente', + }, + languageDescription: { + 'en-us': 'Determines field captions, usage notes and table captions.', + 'ru-ru': + 'Определяет заголовки полей, примечания по использованию и заголовки таблиц.', + 'es-es': 'Determina títulos de campos, notas de uso y títulos de tablas.', + 'fr-fr': + "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux.", + 'uk-ua': + 'Визначає підписи полів, примітки щодо використання та підписи таблиць.', + 'de-ch': + 'Legt Feldbeschriftungen, Verwendungshinweise und Tabellenbeschriftungen fest.', + 'pt-br': 'Determina legendas de campo, notas de uso e legendas de tabela.', + }, + behavior: { + 'en-us': 'Behavior', + 'ru-ru': 'Поведение', + 'es-es': 'Comportamiento', + 'fr-fr': 'Comportement', + 'uk-ua': 'Поведінка', + 'de-ch': 'Verhalten', + 'pt-br': 'Comportamento', + }, + noRestrictionsMode: { + 'en-us': 'No restrictions mode', + 'ru-ru': 'Режим без ограничений', + 'es-es': 'Modo sin restricciones', + 'fr-fr': 'Mode sans restriction', + 'uk-ua': 'Режим без обмежень', + 'de-ch': 'Modus „Keine Einschränkungen“', + 'pt-br': 'Modo sem restrições', + }, + noRestrictionsModeWbDescription: { + 'en-us': 'Allows uploading data to any field in any table.', + 'ru-ru': 'Позволяет загружать данные в любое поле любой таблицы.', + 'es-es': 'Permite cargar datos a cualquier campo de cualquier tabla.', + 'fr-fr': + "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", + 'uk-ua': 'Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.', + 'pt-br': 'Permite carregar dados em qualquer campo de qualquer tabela.', + }, + noRestrictionsModeQueryDescription: { + 'en-us': 'Allows querying data from any field in any table.', + 'ru-ru': 'Позволяет запрашивать данные из любого поля любой таблицы.', + 'es-es': 'Permite consultar datos de cualquier campo de cualquier tabla.', + 'fr-fr': + "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", + 'uk-ua': 'Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.', + 'pt-br': 'Permite consultar dados de qualquer campo em qualquer tabela.', + }, + noRestrictionsModeWarning: { + 'en-us': + 'WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.', + 'ru-ru': + 'ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.', + 'es-es': + 'ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.', + 'uk-ua': + 'ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.', + 'de-ch': + 'WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.', + 'fr-fr': + "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", + 'pt-br': + 'AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.', + }, + adminsOnlyPreference: { + 'en-us': "You don't have permission to change this option", + 'ru-ru': 'У вас нет разрешения на изменение этой опции.', + 'es-es': 'No tienes permiso para cambiar esta opción', + 'fr-fr': "Vous n'êtes pas autorisé à modifier cette option", + 'uk-ua': 'Ви не маєте дозволу змінювати цей параметр', + 'de-ch': 'Sie haben keine Berechtigung, diese Option zu ändern', + 'pt-br': 'Você não tem permissão para alterar esta opção', + }, + stickyScrolling: { + 'en-us': 'Sticky scroll bar', + 'ru-ru': 'Липкая полоса прокрутки', + 'es-es': 'Barra de desplazamiento fija', + 'fr-fr': 'Barre de défilement collante', + 'uk-ua': 'Липка смуга прокрутки', + 'de-ch': 'Klebrige Bildlaufleiste', + 'pt-br': 'Barra de rolagem fixa', + }, + spreadsheet: { + 'en-us': 'Spreadsheet', + 'ru-ru': 'Электронная таблица', + 'es-es': 'Hoja de cálculo', + 'fr-fr': 'Tableur', + 'uk-ua': 'Електронна таблиця', + 'de-ch': 'Kalkulationstabelle', + 'pt-br': 'Planilha', + }, + minSpareRows: { + 'en-us': 'Number of blank rows at the end', + 'ru-ru': 'Количество пустых строк в конце', + 'es-es': 'Número de filas en blanco al final', + 'fr-fr': 'Nombre de lignes vides à la fin', + 'uk-ua': 'Кількість порожніх рядків у кінці', + 'de-ch': 'Anzahl der leeren Zeilen am Ende', + 'pt-br': 'Número de linhas em branco no final', + }, + autoWrapCols: { + 'en-us': 'Navigate to the other side when reaching the edge column', + 'ru-ru': 'Достигнув крайней колонны, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la columna del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la colonne de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете краю колонки', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a coluna da borda', + }, + autoWrapRows: { + 'en-us': 'Navigate to the other side when reaching the edge row', + 'ru-ru': 'Достигнув крайнего ряда, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la fila del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la rangée de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете крайнього ряду', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a fileira de bordas', + }, + enterBeginsEditing: { + 'en-us': 'Enter key begins editing cell', + 'ru-ru': 'Клавиша Enter начинает редактирование ячейки.', + 'es-es': 'La tecla Enter inicia la edición de la celda', + 'fr-fr': 'La touche Entrée commence à modifier la cellule', + 'uk-ua': 'Клавіша Enter починає редагування клітинки', + 'de-ch': 'Mit der Eingabetaste beginnt die Bearbeitung der Zelle', + 'pt-br': 'A tecla Enter inicia a edição da célula', + }, + tabMoveDirection: { + 'en-us': 'Direction of movement when Tab key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Tab', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Tab', + 'fr-fr': + 'Sens de déplacement lorsque la touche Tabulation est enfoncée', + 'uk-ua': 'Напрямок руху при натисканні клавіші Tab', + 'de-ch': 'Bewegungsrichtung beim Drücken der Tab-Taste', + 'pt-br': 'Direção do movimento quando a tecla Tab é pressionada', + }, + tabMoveDirectionDescription: { + 'en-us': + 'You can move in the opposite direction by pressing Shift+Tab.', + 'ru-ru': + 'Вы можете двигаться в обратном направлении, нажав Shift+Tab.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Tab.', + 'fr-fr': + 'Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.', + 'uk-ua': + 'Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Tab.', + }, + column: { + 'en-us': 'Column', + 'ru-ru': 'Столбец', + 'es-es': 'Columna', + 'fr-fr': 'Colonne', + 'uk-ua': 'Колонка', + 'de-ch': 'Spalte', + 'pt-br': 'Coluna', + }, + row: { + 'en-us': 'Row', + 'ru-ru': 'Ряд', + 'es-es': 'Fila', + 'fr-fr': 'Rangée', + 'uk-ua': 'рядок', + 'de-ch': 'Reihe', + 'pt-br': 'Linha', + }, + enterMoveDirection: { + 'en-us': 'Direction of movement when Enter key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Enter', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Enter', + 'uk-ua': 'Напрямок руху, коли натиснуто клавішу Enter', + 'de-ch': 'Bewegungsrichtung beim Drücken der Taste Enter', + 'fr-fr': + 'Direction du mouvement lorsque la touche Entrer est enfoncée', + 'pt-br': + 'Direção do movimento quando a tecla Enter é pressionada', + }, + enterMoveDirectionDescription: { + 'en-us': + 'You can move in the opposite direction by pressing Shift+Enter.', + 'ru-ru': + 'Вы можете двигаться в противоположном направлении, нажав Shift+Enter.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Enter.', + 'fr-fr': 'Synonyme couleur.', + 'uk-ua': + 'Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Enter.', + }, + filterPickLists: { + 'en-us': 'Filter pick list items', + 'ru-ru': 'Фильтрация элементов списка выбора', + 'es-es': 'Filtrar elementos de la lista de selección', + 'fr-fr': 'Filtrer les éléments de la liste de sélection', + 'uk-ua': 'Фільтр вибору елементів списку', + 'de-ch': 'Auswahllistenelemente filtern', + 'pt-br': 'Filtrar itens da lista de seleção', + }, + exportFileDelimiter: { + 'en-us': 'Export file delimiter', + 'ru-ru': 'Разделитель файлов экспорта', + 'es-es': 'Delimitador de archivo de exportación', + 'fr-fr': "Délimiteur de fichier d'exportation", + 'uk-ua': 'Роздільник файлу експорту', + 'de-ch': 'Dateitrennzeichen exportieren', + 'pt-br': 'Delimitador de arquivo de exportação', + }, + exportCsvUtf8Bom: { + 'en-us': 'Add UTF-8 BOM to CSV file exports', + 'ru-ru': 'Добавить UTF-8 BOM в экспорт CSV-файла', + 'es-es': 'Agregar BOM UTF-8 a las exportaciones de archivos CSV', + 'fr-fr': 'Ajouter UTF-8 BOM aux exportations de fichiers CSV', + 'uk-ua': 'Додайте специфікацію UTF-8 до експорту файлу CSVу', + 'de-ch': 'UTF-8 BOM zum CSV-Dateiexport hinzufügen', + 'pt-br': 'Adicionar UTF-8 BOM às exportações de arquivos CSV', + }, + exportCsvUtf8BomDescription: { + 'en-us': + 'Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.', + 'ru-ru': 'Корректное отображение экспортированных CSV-файлов в Excel.', + 'es-es': + 'Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.', + 'fr-fr': + "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", + 'uk-ua': 'Змушує експорт файлів CSV правильно відображатися в Excel.', + 'de-ch': + 'Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.', + 'pt-br': + 'Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.', + }, + caseSensitive: { + 'en-us': 'Case-sensitive', + 'ru-ru': 'С учетом регистра', + 'es-es': 'Distingue mayúsculas y minúsculas', + 'fr-fr': 'Sensible aux majuscules et minuscules', + 'uk-ua': 'Чутливий до регістру', + 'de-ch': 'Groß- und Kleinschreibung beachten', + 'pt-br': 'Maiúsculas e minúsculas', + }, + caseInsensitive: { + 'en-us': 'Case-insensitive', + 'ru-ru': 'Без учета регистра', + 'es-es': 'No distingue entre mayúsculas y minúsculas', + 'fr-fr': 'Insensible à la casse', + 'uk-ua': 'Регістр не враховується', + 'de-ch': 'Groß- und Kleinschreibung wird nicht berücksichtigt', + 'pt-br': 'Não diferencia maiúsculas de minúsculas', + }, + showNoReadTables: { + 'en-us': 'Show tables without "Read" access', + 'ru-ru': 'Показывать таблицы без доступа «Чтение»', + 'es-es': 'Mostrar tablas sin acceso de "Lectura"', + 'fr-fr': 'Afficher les tableaux sans accès "Lecture"', + 'uk-ua': 'Показувати таблиці без доступу «Читання»', + 'de-ch': 'Tabellen ohne Lesezugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso de "Leitura"', + }, + showNoAccessTables: { + 'en-us': 'Show tables without "Create" access', + 'ru-ru': 'Показывать таблицы без права «Создать»', + 'es-es': 'Mostrar tablas sin acceso "Crear"', + 'fr-fr': 'Afficher les tableaux sans accès "Créer"', + 'uk-ua': 'Показувати таблиці без доступу «Створити»', + 'de-ch': 'Tabellen ohne „Erstellen“-Zugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso "Criar"', + }, + textAreaAutoGrow: { + 'en-us': 'Text boxes grow automatically', + 'ru-ru': 'Текстовые поля увеличиваются автоматически', + 'es-es': 'Los cuadros de texto crecen automáticamente', + 'fr-fr': "Les zones de texte s'agrandissent automatiquement", + 'uk-ua': 'Текстові поля збільшуються автоматично', + 'de-ch': 'Textfelder werden automatisch vergrößert', + 'pt-br': 'As caixas de texto crescem automaticamente', + }, + clearQueryFilters: { + 'en-us': 'Reset query filters', + 'ru-ru': 'Сбросить фильтры запроса', + 'es-es': 'Restablecer filtros de consulta', + 'fr-fr': 'Réinitialiser les filtres de requête', + 'uk-ua': 'Скинути фільтри запитів', + 'de-ch': 'Abfragefilter zurücksetzen', + 'pt-br': 'Redefinir filtros de consulta', + }, + clearQueryFiltersDescription: { + 'en-us': 'Clears all query filters when running a Report from a Form.', + 'de-ch': + 'Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.', + 'es-es': + 'Borra todos los filtros de consulta al ejecutar un informe desde un formulario.', + 'fr-fr': + "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", + 'ru-ru': 'Очищает все фильтры запроса при запуске отчета из формы.', + 'uk-ua': 'Очищає всі фільтри запитів під час запуску звіту з форми.', + 'pt-br': + 'Limpa todos os filtros de consulta ao executar um relatório de um formulário.', + }, + queryParamtersFromForm: { + 'en-us': 'Show query filters when running a Report from a Form', + 'de-ch': + 'Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird', + 'es-es': + 'Mostrar filtros de consulta al ejecutar un informe desde un formulario', + 'fr-fr': + "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", + 'ru-ru': 'Показывать фильтры запроса при запуске отчета из формы', + 'uk-ua': 'Показувати фільтри запитів під час запуску звіту з форми', + 'pt-br': + 'Mostrar filtros de consulta ao executar um relatório de um formulário', + }, + autoGrowAutoComplete: { + 'en-us': 'Allow autocomplete to grow as wide as need', + 'ru-ru': + 'Разрешить автозаполнению расширяться настолько, насколько это необходимо', + 'es-es': 'Permitir que el autocompletado crezca tanto como sea necesario', + 'fr-fr': + 'Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée', + 'uk-ua': + 'Дозволити автозаповнення розширюватися настільки, наскільки потрібно', + 'de-ch': + 'Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen', + 'pt-br': + 'Permitir que o preenchimento automático cresça o quanto for necessário', + }, + tableNameInTitle: { + 'en-us': 'Include table name in the browser page title', + 'ru-ru': 'Включить имя таблицы в заголовок страницы браузера', + 'es-es': + 'Incluir el nombre de la tabla en el título de la página del navegador', + 'fr-fr': + 'Inclure le nom de la table dans le titre de la page du navigateur', + 'uk-ua': 'Включіть назву таблиці в заголовок сторінки браузера', + 'de-ch': 'Tabellennamen in den Seitentitel des Browsers aufnehmen', + 'pt-br': 'Incluir nome da tabela no título da página do navegador', + }, + focusFirstField: { + 'en-us': 'Focus first field', + 'de-ch': 'Fokus erstes Feld', + 'es-es': 'Enfoque el primer campo', + 'fr-fr': 'Concentrez-vous sur le premier champ', + 'ru-ru': 'Фокус первого поля', + 'uk-ua': 'Перейти до першого поля', + 'pt-br': 'Foco primeiro campo', + }, + doubleClickZoom: { + 'en-us': 'Double click to zoom', + 'ru-ru': 'Дважды щелкните, чтобы увеличить', + 'es-es': 'Haga doble clic para ampliar', + 'fr-fr': 'Double-cliquez pour zoomer', + 'uk-ua': 'Двічі клацніть, щоб збільшити', + 'de-ch': 'Zum Vergrößern doppelklicken', + 'pt-br': 'Clique duas vezes para ampliar', + }, + closePopupOnClick: { + 'en-us': 'Close pop-up on outside click', + 'ru-ru': 'Закрытие всплывающего окна при внешнем щелчке', + 'es-es': 'Cerrar ventana emergente al hacer clic desde fuera', + 'fr-fr': "Fermer la pop-up lors d'un clic extérieur", + 'uk-ua': 'Закрити спливаюче вікно при зовнішньому клацанні', + 'de-ch': 'Popup bei externem Klick schließen', + 'pt-br': 'Fechar pop-up ao clicar fora', + }, + animateTransitions: { + 'en-us': 'Animate transitions', + 'ru-ru': 'Анимированные переходы', + 'es-es': 'Animar transiciones', + 'fr-fr': 'Animer les transitions', + 'uk-ua': 'Анімація переходів', + 'de-ch': 'Übergänge animieren', + 'pt-br': 'Transições animadas', + }, + panInertia: { + 'en-us': 'Pan inertia', + 'ru-ru': 'Инерция пан', + 'es-es': 'Inercia de la sartén', + 'fr-fr': 'Inertie du bac', + 'uk-ua': 'Інерція панорами', + 'de-ch': 'Schwenkträgheit', + 'pt-br': 'Inércia da panela', + }, + mouseDrags: { + 'en-us': 'Mouse drags', + 'ru-ru': 'Перетаскивание мышью', + 'es-es': 'El ratón arrastra', + 'uk-ua': 'Виділіть відповідний підрядок', + 'de-ch': 'Maus zieht', + 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', + 'pt-br': 'Arrastos do mouse', + }, + scrollWheelZoom: { + 'en-us': 'Scroll wheel zoom', + 'ru-ru': 'Масштабирование с помощью колеса прокрутки', + 'es-es': 'Zoom con rueda de desplazamiento', + 'fr-fr': 'Zoom avec la molette de défilement', + 'uk-ua': 'Масштаб колеса прокрутки', + 'de-ch': 'Scrollrad-Zoom', + 'pt-br': 'Zoom da roda de rolagem', + }, + flexibleColumnWidth: { + 'en-us': 'Flexible column width', + 'ru-ru': 'Гибкая ширина столбца', + 'es-es': 'Ancho de columna flexible', + 'fr-fr': 'Largeur de colonne flexible', + 'uk-ua': 'Гнучка ширина колонки', + 'de-ch': 'Flexible Spaltenbreite', + 'pt-br': 'Largura de coluna flexível', + }, + flexibleSubGridColumnWidth: { + 'en-us': 'Flexible subview grid column width', + 'ru-ru': 'Гибкая ширина столбца сетки подпредставлений', + 'es-es': 'Ancho de columna de cuadrícula de subvista flexible', + 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', + 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', + 'de-ch': 'Flexible Rasterspaltenbreite der Unteransicht', + 'pt-br': 'Largura flexível da coluna da grade de subvisualização', + }, + closeOnEsc: { + 'en-us': 'Close on ESC key press', + 'ru-ru': 'Закрыть нажатием клавиши ESC', + 'es-es': 'Cerrar al presionar la tecla ESC', + 'fr-fr': 'Icône et nom de la table', + 'uk-ua': 'Закриття натисканням клавіші ESC', + 'de-ch': 'Schließen durch Drücken der Taste ESC', + 'pt-br': 'Fechar ao pressionar a tecla ESC', + }, + closeOnOutsideClick: { + 'en-us': 'Close on outside click', + 'ru-ru': 'Закрытие по внешнему щелчку', + 'es-es': 'Cerrar al hacer clic desde fuera', + 'fr-fr': 'Fermer sur clic extérieur', + 'uk-ua': 'Закрийте зовнішнім клацанням', + 'de-ch': 'Schließen durch Klicken von außen', + 'pt-br': 'Fechar com clique externo', + }, + scopeEntireTablePicklists: { + 'en-us': 'Scope "Entire Table" picklists', + }, + scopeEntireTablePicklistsDescription: { + 'en-us': + 'Restrict "Entire Table" picklists to values used by records in this collection.', + }, + catalogNumberInheritanceDescription: { + 'en-us': + 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', + }, + catalogNumberParentInheritanceDescription: { + 'en-us': + 'Control whether component records inherit catalog numbers from their parent Collection Object.', + }, + useAccessibleFullDatePicker: { + 'en-us': 'Use accessible full date picker', + 'ru-ru': 'Используйте доступный полный выбор даты', + 'es-es': 'Utilice el selector de fecha completo y accesible', + 'fr-fr': 'Utiliser un sélecteur de date complet accessible', + 'uk-ua': 'Використовуйте доступний повний засіб вибору дати', + 'de-ch': 'Verwenden Sie eine barrierefreie Datumsauswahl', + 'pt-br': 'Use o seletor de data completo acessível', + }, + useAccessibleMonthPicker: { + 'en-us': 'Use accessible month picker', + 'ru-ru': 'Используйте доступный выбор месяца', + 'es-es': 'Utilice el selector de meses accesible', + 'fr-fr': 'Utiliser le sélecteur de mois accessible', + 'uk-ua': 'Використовуйте доступний засіб вибору місяця', + 'de-ch': 'Verwenden Sie die barrierefreie Monatsauswahl', + 'pt-br': 'Use o seletor de meses acessível', + }, + collectionSortOrderDescription: { + 'en-us': 'This determines the visual order of collections.', + 'ru-ru': 'Это определяет визуальный порядок коллекций.', + 'es-es': 'Esto determina el orden visual de las colecciones.', + 'fr-fr': "Ceci détermine l'ordre visuel des collections.", + 'uk-ua': 'Це визначає візуальний порядок колекцій.', + 'de-ch': 'Dies bestimmt die visuelle Reihenfolge der Sammlungen.', + 'pt-br': 'Isso determina a ordem visual das coleções.', + }, + recordSetRecordToOpen: { + 'en-us': 'Record to open by default', + 'ru-ru': 'Запись для открытия по умолчанию', + 'es-es': 'Registro para abrir por defecto', + 'fr-fr': 'Enregistrement à ouvrir par défaut', + 'uk-ua': 'Запис відкривається за умовчанням', + 'de-ch': 'Standardmäßig zu öffnender Datensatz', + 'pt-br': 'Gravar para abrir por padrão', + }, +altClickToSupressNewTab: { 'en-us': '{altKeyName:string}+Click to suppress new tab', 'ru-ru': @@ -67,98 +798,6 @@ export const preferencesBehaviorStrings = { 'ru-ru': 'Сортировать по полю', 'uk-ua': 'Сортувати за полем', }, - lineWrap: { - 'en-us': 'Line wrap', - 'ru-ru': 'Перенос строки', - 'es-es': 'Ajuste de línea', - 'fr-fr': 'Retour à la ligne', - 'uk-ua': 'Обтікання лініями', - 'de-ch': 'Zeilenumbruch', - 'pt-br': 'Quebra de linha', - }, - indentSize: { - 'en-us': 'Indent size', - 'ru-ru': 'Размер отступа', - 'es-es': 'Tamaño de sangría', - 'fr-fr': 'Taille du retrait', - 'uk-ua': 'Розмір відступу', - 'de-ch': 'Einzugsgröße', - 'pt-br': 'Tamanho do recuo', - }, - indentWithTab: { - 'en-us': 'Indent with Tab', - 'ru-ru': 'Отступ с помощью Tab', - 'es-es': 'Sangría con Tab', - 'fr-fr': 'Indenter avec Tabulation', - 'uk-ua': 'Відступ із Tab', - 'de-ch': 'Einrücken mit Tab', - 'pt-br': 'Recuo com Tab', - }, - formHeaderFormat: { - 'en-us': 'Form header format', - 'ru-ru': 'Формат заголовка формы', - 'es-es': 'Formato del encabezado del formulario', - 'fr-fr': "Format d'en-tête de formulaire", - 'uk-ua': 'Формат заголовка форми', - 'de-ch': 'Formularkopfformat', - 'pt-br': 'Formato do cabeçalho do formulário', - }, - iconAndTableName: { - 'en-us': 'Icon and table name', - 'ru-ru': 'Значок и название таблицы', - 'es-es': 'Icono y nombre de la tabla', - 'fr-fr': 'Icône et nom de la table', - 'uk-ua': 'Значок і назва таблиці', - 'de-ch': 'Symbol und Tabellenname', - 'pt-br': 'Ícone e nome da tabela', - }, - tableIcon: { - 'en-us': 'Table icon', - 'ru-ru': 'Значок таблицы', - 'es-es': 'Icono de tabla', - 'fr-fr': 'Icône de tableau', - 'uk-ua': 'Значок таблиці', - 'de-ch': 'Tabellensymbol', - 'pt-br': 'Ícone de tabela', - }, - maxHeight: { - 'en-us': 'Max height', - 'ru-ru': 'Максимальная высота', - 'es-es': 'Altura máxima', - 'fr-fr': 'hauteur maximum', - 'uk-ua': 'Максимальна висота', - 'de-ch': 'Maximale Höhe', - 'pt-br': 'Altura máxima', - }, - autoComplete: { - 'en-us': 'Auto complete', - 'ru-ru': 'Автозаполнение', - 'es-es': 'Autocompletar', - 'fr-fr': - "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux", - 'uk-ua': - 'Визначає підписи полів, примітки щодо використання та підписи таблиць', - 'de-ch': 'Autovervollständigung', - 'pt-br': 'Preenchimento automático', - }, - searchCaseSensitive: { - 'en-us': 'Case-sensitive search', - 'es-es': 'Búsqueda que distingue entre mayúsculas y minúsculas', - 'fr-fr': 'Recherche sensible à la casse', - 'uk-ua': 'Пошук з урахуванням регістру', - 'de-ch': 'Groß- und Kleinschreibung beachten', - 'ru-ru': 'Поиск с учетом регистра', - 'pt-br': 'Pesquisa com diferenciação entre maiúsculas e minúsculas', - }, - searchField: { - 'en-us': 'Search Field', - 'ru-ru': 'Поле поиска', - 'es-es': 'Campo de búsqueda', - 'fr-fr': 'Champ de recherche', - 'uk-ua': 'Поле пошуку', - 'de-ch': 'Suchfeld', - 'pt-br': 'Campo de pesquisa', - }, createInteractions: { 'en-us': 'Creating an interaction', 'ru-ru': 'Создание взаимодействия', @@ -440,148 +1079,6 @@ export const preferencesBehaviorStrings = { 'uk-ua': 'Розділіть довгі рядки XML на кілька рядків', 'pt-br': 'Dividir longas linhas de XML em várias linhas', }, - url: { - 'en-us': 'URL', - 'de-ch': 'URL', - 'es-es': 'URL', - 'fr-fr': 'URL', - 'uk-ua': 'URL', - 'ru-ru': 'URL', - 'pt-br': 'URL', - }, - pickAttachment: { - 'en-us': 'Pick an attachment', - 'es-es': 'Elige un archivo adjunto', - 'fr-fr': 'Choisissez une pièce jointe', - 'ru-ru': 'Выберите вложение', - 'uk-ua': 'Виберіть вкладення', - 'de-ch': 'Wählen Sie einen Anhang', - 'pt-br': 'Escolha um anexo', - }, - attachmentFailed: { - 'en-us': 'The attachment failed to load.', - 'de-ch': 'Der Anhang konnte nicht geladen werden.', - 'es-es': 'No se pudo cargar el archivo adjunto.', - 'fr-fr': "La pièce jointe n'a pas pu être chargée.", - 'ru-ru': 'Не удалось загрузить вложение.', - 'uk-ua': 'Не вдалося завантажити вкладений файл.', - 'pt-br': 'O anexo não pôde ser carregado.', - }, - pickImage: { - 'en-us': 'Pick an image', - 'de-ch': 'Wählen Sie ein Bild aus', - 'es-es': 'Elige una imagen', - 'fr-fr': 'Choisissez une image', - 'ru-ru': 'Выберите изображение', - 'uk-ua': 'Виберіть зображення', - 'pt-br': 'Escolha uma imagem', - }, - customLogo: { - 'en-us': 'Expanded Image URL', - 'de-ch': 'Erweiterte Bild-URL', - 'es-es': 'URL de imagen expandida', - 'fr-fr': "URL de l'image étendue", - 'ru-ru': 'URL-адрес развернутого изображения', - 'uk-ua': 'Розширена URL-адреса зображення', - 'pt-br': 'URL da imagem expandida', - }, - customLogoCollapsed: { - 'en-us': 'Collapsed Image URL', - 'de-ch': 'URL des minimierten Bildes', - 'es-es': 'URL de imagen contraída', - 'fr-fr': "URL de l'image réduite", - 'ru-ru': 'URL-адрес свернутого изображения', - 'uk-ua': 'URL-адреса згорнутого зображення', - 'pt-br': 'URL da imagem recolhida', - }, - customLogoDescription: { - 'en-us': - 'A URL to an image that would be displayed next to the Specify logo in the navigation menu.', - 'de-ch': - 'Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.', - 'es-es': - 'Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.', - 'fr-fr': - 'Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.', - 'ru-ru': - 'URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.', - 'uk-ua': - 'URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.', - 'pt-br': - 'Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.', - }, - showLineNumber: { - 'en-us': 'Show query result line number', - 'de-ch': 'Zeilennummer des Abfrageergebnisses anzeigen', - 'es-es': 'Mostrar el número de línea del resultado de la consulta', - 'fr-fr': 'Afficher le numéro de ligne du résultat de la requête', - 'ru-ru': 'Показать номер строки результата запроса', - 'uk-ua': 'Показати номер рядка результату запиту', - 'pt-br': 'Mostrar número da linha do resultado da consulta', - }, - saveButtonColor: { - 'en-us': 'Save button color', - 'de-ch': 'Farbe der Schaltfläche „Speichern“', - 'es-es': 'Guardar color del botón', - 'fr-fr': 'Couleur du bouton Enregistrer', - 'ru-ru': 'Сохранить цвет кнопки', - 'uk-ua': 'Зберегти колір кнопки', - 'pt-br': 'Cor do botão Salvar', - }, - secondaryButtonColor: { - 'en-us': 'Secondary button color', - 'es-es': 'Color del botón secundario', - 'fr-fr': 'Couleur du bouton secondaire', - 'ru-ru': 'Цвет вторичной кнопки', - 'uk-ua': 'Колір вторинної кнопки', - 'de-ch': 'Sekundäre Schaltflächenfarbe', - 'pt-br': 'Cor do botão secundário', - }, - secondaryLightButtonColor: { - 'en-us': 'Secondary light button color', - 'de-ch': 'Farbe der sekundären Lichttaste', - 'es-es': 'Color del botón de luz secundaria', - 'fr-fr': 'Couleur du bouton lumineux secondaire', - 'ru-ru': 'Цвет кнопки дополнительного освещения', - 'uk-ua': 'Колір вторинної світлової кнопки', - 'pt-br': 'Cor do botão de luz secundária', - }, - dangerButtonColor: { - 'en-us': 'Danger button color', - 'de-ch': 'Farbe der Gefahrenschaltfläche', - 'es-es': 'Color del botón de peligro', - 'fr-fr': 'Couleur du bouton de danger', - 'ru-ru': 'Цвет кнопки «Опасность»', - 'uk-ua': 'Колір кнопки небезпеки', - 'pt-br': 'Cor do botão de perigo', - }, - infoButtonColor: { - 'en-us': 'Info button color', - 'de-ch': 'Farbe der Info-Schaltfläche', - 'es-es': 'Color del botón de información', - 'fr-fr': "Couleur du bouton d'information", - 'ru-ru': 'Цвет кнопки информации', - 'uk-ua': 'Колір інформаційної кнопки', - 'pt-br': 'Cor do botão de informações', - }, - warningButtonColor: { - 'en-us': 'Warning button color', - 'de-ch': 'Farbe der Warnschaltfläche', - 'es-es': 'Color del botón de advertencia', - 'fr-fr': "Couleur du bouton d'avertissement", - 'ru-ru': 'Цвет кнопки предупреждения', - 'uk-ua': 'Колір кнопки попередження', - 'pt-br': 'Cor do botão de aviso', - }, - successButtonColor: { - 'en-us': 'Success button color', - 'de-ch': 'Farbe der Schaltfläche „Erfolg“', - 'es-es': 'Color del botón de éxito', - 'fr-fr': 'Couleur du bouton de réussite', - 'ru-ru': 'Цвет кнопки «Успех»', - 'uk-ua': 'Колір кнопки успіху', - 'pt-br': 'Cor do botão de sucesso', - }, openAsReadOnly: { 'en-us': 'Open all records in read-only mode', 'de-ch': 'Alle Datensätze im schreibgeschützten Modus öffnen', @@ -643,42 +1140,6 @@ export const preferencesBehaviorStrings = { 'uk-ua': 'Детальний вигляд', 'pt-br': 'Visão detalhada', }, - attachmentPreviewMode: { - 'en-us': 'Attachment preview mode', - 'de-ch': 'Anhangsvorschaumodus', - 'es-es': 'Modo de vista previa de archivos adjuntos', - 'fr-fr': "Mode d'aperçu des pièces jointes", - 'ru-ru': 'Режим предварительного просмотра вложений', - 'uk-ua': 'Режим попереднього перегляду вкладених файлів', - 'pt-br': 'Modo de visualização de anexos', - }, - fullResolution: { - 'en-us': 'Full Resolution', - 'de-ch': 'Volle Auflösung', - 'es-es': 'Resolución completa', - 'fr-fr': 'Pleine résolution', - 'ru-ru': 'Полное разрешение', - 'uk-ua': 'Повна роздільна здатність', - 'pt-br': 'Resolução completa', - }, - thumbnail: { - 'en-us': 'Thumbnail', - 'de-ch': 'Miniaturansicht', - 'es-es': 'Uña del pulgar', - 'fr-fr': 'Vignette', - 'ru-ru': 'Миниатюра', - 'uk-ua': 'Мініатюра', - 'pt-br': 'Miniatura', - }, - addSearchBarHomePage: { - 'en-us': 'Add Search Bar on home page', - 'de-ch': 'Suchleiste auf der Startseite hinzufügen', - 'es-es': 'Agregar barra de búsqueda en la página de inicio', - 'fr-fr': "Ajouter une barre de recherche sur la page d'accueil", - 'ru-ru': 'Добавить панель поиска на домашнюю страницу', - 'uk-ua': 'Додайте рядок пошуку на головну сторінку', - 'pt-br': 'Adicionar barra de pesquisa na página inicial', - }, inheritanceCatNumberPref: { 'en-us': 'Enable the inheritance of the primary catalog number to its empty siblings.', @@ -730,7 +1191,5 @@ export const preferencesBehaviorStrings = { } as const; export const preferencesBehaviorText = createDictionary( - preferencesBehaviorStrings + preferencesBehaviorDictionary ); - -export default preferencesBehaviorText; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts index 22be0918050..aa371bc7816 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.content.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.content.ts @@ -1,11 +1,29 @@ /** - * Localization strings for content and asset preferences. + * Localization strings for content-related preferences. * * @module */ import { createDictionary } from './utils'; - -export const preferencesContentStrings = { +// Refer to "Guidelines for Programmers" in ./README.md before editing this file +export const preferencesContentDictionary = { + displayAuthor: { + 'en-us': 'Show author in the tree', + 'ru-ru': 'Показать автора в дереве', + 'es-es': 'Mostrar autor en el árbol', + 'fr-fr': "Afficher l'auteur dans l'arbre", + 'uk-ua': 'Показати автора в дереві', + 'de-ch': 'Autor im Baum anzeigen', + 'pt-br': 'Mostrar autor', + }, + welcomePage: { + 'en-us': 'Home Page', + 'ru-ru': 'Домашняя страница', + 'es-es': 'Página de inicio', + 'fr-fr': "Page d'accueil", + 'uk-ua': 'Домашня сторінка', + 'de-ch': 'Startseite', + 'pt-br': 'Página inicial', + }, content: { 'en-us': 'Content', 'ru-ru': 'Содержание', @@ -52,551 +70,6 @@ export const preferencesContentStrings = { 'Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:', 'pt-br': 'Um URL para uma página que seria incorporada na página inicial:', }, - behavior: { - 'en-us': 'Behavior', - 'ru-ru': 'Поведение', - 'es-es': 'Comportamiento', - 'fr-fr': 'Comportement', - 'uk-ua': 'Поведінка', - 'de-ch': 'Verhalten', - 'pt-br': 'Comportamento', - }, - noRestrictionsMode: { - 'en-us': 'No restrictions mode', - 'ru-ru': 'Режим без ограничений', - 'es-es': 'Modo sin restricciones', - 'fr-fr': 'Mode sans restriction', - 'uk-ua': 'Режим без обмежень', - 'de-ch': 'Modus „Keine Einschränkungen“', - 'pt-br': 'Modo sem restrições', - }, - noRestrictionsModeWbDescription: { - 'en-us': 'Allows uploading data to any field in any table.', - 'ru-ru': 'Позволяет загружать данные в любое поле любой таблицы.', - 'es-es': 'Permite cargar datos a cualquier campo de cualquier tabla.', - 'fr-fr': - "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", - 'uk-ua': 'Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.', - 'de-ch': - 'Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.', - 'pt-br': 'Permite carregar dados em qualquer campo de qualquer tabela.', - }, - noRestrictionsModeQueryDescription: { - 'en-us': 'Allows querying data from any field in any table.', - 'ru-ru': 'Позволяет запрашивать данные из любого поля любой таблицы.', - 'es-es': 'Permite consultar datos de cualquier campo de cualquier tabla.', - 'fr-fr': - "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", - 'uk-ua': 'Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.', - 'de-ch': - 'Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.', - 'pt-br': 'Permite consultar dados de qualquer campo em qualquer tabela.', - }, - noRestrictionsModeWarning: { - 'en-us': - 'WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.', - 'ru-ru': - 'ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.', - 'es-es': - 'ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.', - 'uk-ua': - 'ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.', - 'de-ch': - 'WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.', - 'fr-fr': - "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", - 'pt-br': - 'AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.', - }, - adminsOnlyPreference: { - 'en-us': "You don't have permission to change this option", - 'ru-ru': 'У вас нет разрешения на изменение этой опции.', - 'es-es': 'No tienes permiso para cambiar esta opción', - 'fr-fr': "Vous n'êtes pas autorisé à modifier cette option", - 'uk-ua': 'Ви не маєте дозволу змінювати цей параметр', - 'de-ch': 'Sie haben keine Berechtigung, diese Option zu ändern', - 'pt-br': 'Você não tem permissão para alterar esta opção', - }, - stickyScrolling: { - 'en-us': 'Sticky scroll bar', - 'ru-ru': 'Липкая полоса прокрутки', - 'es-es': 'Barra de desplazamiento fija', - 'fr-fr': 'Barre de défilement collante', - 'uk-ua': 'Липка смуга прокрутки', - 'de-ch': 'Klebrige Bildlaufleiste', - 'pt-br': 'Barra de rolagem fixa', - }, - foreground: { - 'en-us': 'Foreground', - 'ru-ru': 'Передний план', - 'es-es': 'Primer plano', - 'fr-fr': 'Premier plan', - 'uk-ua': 'Передній план', - 'de-ch': 'Vordergrund', - 'pt-br': 'Primeiro plano', - }, - background: { - 'en-us': 'Background', - 'ru-ru': 'Фон', - 'es-es': 'Fondo', - 'fr-fr': 'Arrière-plan', - 'uk-ua': 'Фон', - 'de-ch': 'Hintergrund', - 'pt-br': 'Fundo', - }, - sidebarTheme: { - 'en-us': 'Sidebar theme', - 'de-ch': 'Seitenleistenthema', - 'es-es': 'Tema de la barra lateral', - 'fr-fr': 'Thème de la barre latérale', - 'ru-ru': 'Тема боковой панели', - 'uk-ua': 'Тема бічної панелі', - 'pt-br': 'Tema da barra lateral', - }, - darkForeground: { - 'en-us': 'Foreground (dark theme)', - 'ru-ru': 'Передний план (тёмная тема)', - 'es-es': 'Primer plano (tema oscuro)', - 'fr-fr': 'Premier plan (thème sombre)', - 'uk-ua': 'Передній план (темна тема)', - 'de-ch': 'Vordergrund (dunkles Design)', - 'pt-br': 'Primeiro plano (tema escuro)', - }, - darkBackground: { - 'en-us': 'Background (dark theme)', - 'ru-ru': 'Фон (тёмная тема)', - 'es-es': 'Fondo (tema oscuro)', - 'fr-fr': 'Arrière-plan (thème sombre)', - 'uk-ua': 'Фон (темна тема)', - 'de-ch': 'Hintergrund (dunkles Design)', - 'pt-br': 'Plano de fundo (tema escuro)', - }, - accentColor1: { - 'en-us': 'Accent color 1', - 'ru-ru': 'Акцентный цвет 1', - 'es-es': 'Color de acento 1', - 'fr-fr': "Couleur d'accent 1", - 'uk-ua': 'Акцентний колір 1', - 'de-ch': 'Akzentfarbe 1', - 'pt-br': 'Cor de destaque 1', - }, - accentColor2: { - 'en-us': 'Accent color 2', - 'ru-ru': 'Акцентный цвет 2', - 'es-es': 'Color de acento 2', - 'fr-fr': "Couleur d'accent 2", - 'uk-ua': 'Акцентний колір 2', - 'de-ch': 'Akzentfarbe 2', - 'pt-br': 'Cor de destaque 2', - }, - accentColor3: { - 'en-us': 'Accent color 3', - 'ru-ru': 'Акцентный цвет 3', - 'es-es': 'Color de acento 3', - 'fr-fr': "Couleur d'accent 3", - 'uk-ua': 'Акцентний колір 3', - 'de-ch': 'Akzentfarbe 3', - 'pt-br': 'Cor de destaque 3', - }, - accentColor4: { - 'en-us': 'Accent color 4', - 'ru-ru': 'Акцентный цвет 4', - 'es-es': 'Color de acento 4', - 'fr-fr': "Couleur d'accent 4", - 'uk-ua': 'Акцентний колір 4', - 'de-ch': 'Akzentfarbe 4', - 'pt-br': 'Cor de destaque 4', - }, - accentColor5: { - 'en-us': 'Accent color 5', - 'ru-ru': 'Акцентный цвет 5', - 'es-es': 'Color de acento 5', - 'fr-fr': "Couleur d'accent 5", - 'uk-ua': 'Акцентний колір 5', - 'de-ch': 'Akzentfarbe 5', - 'pt-br': 'Cor de destaque 5', - }, - spreadsheet: { - 'en-us': 'Spreadsheet', - 'ru-ru': 'Электронная таблица', - 'es-es': 'Hoja de cálculo', - 'fr-fr': 'Tableur', - 'uk-ua': 'Електронна таблиця', - 'de-ch': 'Kalkulationstabelle', - 'pt-br': 'Planilha', - }, - minSpareRows: { - 'en-us': 'Number of blank rows at the end', - 'ru-ru': 'Количество пустых строк в конце', - 'es-es': 'Número de filas en blanco al final', - 'fr-fr': 'Nombre de lignes vides à la fin', - 'uk-ua': 'Кількість порожніх рядків у кінці', - 'de-ch': 'Anzahl der leeren Zeilen am Ende', - 'pt-br': 'Número de linhas em branco no final', - }, - autoWrapCols: { - 'en-us': 'Navigate to the other side when reaching the edge column', - 'ru-ru': 'Достигнув крайней колонны, перейдите на другую сторону.', - 'es-es': 'Navegue hacia el otro lado al llegar a la columna del borde.', - 'fr-fr': - 'Naviguez de l’autre côté lorsque vous atteignez la colonne de bord', - 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете краю колонки', - 'de-ch': - 'Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen', - 'pt-br': 'Navegue para o outro lado ao atingir a coluna da borda', - }, - autoWrapRows: { - 'en-us': 'Navigate to the other side when reaching the edge row', - 'ru-ru': 'Достигнув крайнего ряда, перейдите на другую сторону.', - 'es-es': 'Navegue hacia el otro lado al llegar a la fila del borde.', - 'fr-fr': - 'Naviguez de l’autre côté lorsque vous atteignez la rangée de bord', - 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете крайнього ряду', - 'de-ch': - 'Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen', - 'pt-br': 'Navegue para o outro lado ao atingir a fileira de bordas', - }, - enterBeginsEditing: { - 'en-us': 'Enter key begins editing cell', - 'ru-ru': 'Клавиша Enter начинает редактирование ячейки.', - 'es-es': 'La tecla Enter inicia la edición de la celda', - 'fr-fr': 'La touche Entrée commence à modifier la cellule', - 'uk-ua': 'Клавіша Enter починає редагування клітинки', - 'de-ch': 'Mit der Eingabetaste beginnt die Bearbeitung der Zelle', - 'pt-br': 'A tecla Enter inicia a edição da célula', - }, - tabMoveDirection: { - 'en-us': 'Direction of movement when Tab key is pressed', - 'ru-ru': 'Направление движения при нажатии клавиши Tab', - 'es-es': - 'Dirección de movimiento cuando se presiona la tecla Tab', - 'fr-fr': - 'Sens de déplacement lorsque la touche Tabulation est enfoncée', - 'uk-ua': 'Напрямок руху при натисканні клавіші Tab', - 'de-ch': 'Bewegungsrichtung beim Drücken der Tab-Taste', - 'pt-br': 'Direção do movimento quando a tecla Tab é pressionada', - }, - tabMoveDirectionDescription: { - 'en-us': - 'You can move in the opposite direction by pressing Shift+Tab.', - 'ru-ru': - 'Вы можете двигаться в обратном направлении, нажав Shift+Tab.', - 'es-es': - 'Puedes moverte en la dirección opuesta presionando Shift+Tab.', - 'fr-fr': - 'Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.', - 'uk-ua': - 'Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.', - 'de-ch': - 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.', - 'pt-br': - 'Você pode mover na direção oposta pressionando Shift+Tab.', - }, - column: { - 'en-us': 'Column', - 'ru-ru': 'Столбец', - 'es-es': 'Columna', - 'fr-fr': 'Colonne', - 'uk-ua': 'Колонка', - 'de-ch': 'Spalte', - 'pt-br': 'Coluna', - }, - row: { - 'en-us': 'Row', - 'ru-ru': 'Ряд', - 'es-es': 'Fila', - 'fr-fr': 'Rangée', - 'uk-ua': 'рядок', - 'de-ch': 'Reihe', - 'pt-br': 'Linha', - }, - enterMoveDirection: { - 'en-us': 'Direction of movement when Enter key is pressed', - 'ru-ru': 'Направление движения при нажатии клавиши Enter', - 'es-es': - 'Dirección de movimiento cuando se presiona la tecla Enter', - 'uk-ua': 'Напрямок руху, коли натиснуто клавішу Enter', - 'de-ch': 'Bewegungsrichtung beim Drücken der Taste Enter', - 'fr-fr': - 'Direction du mouvement lorsque la touche Entrer est enfoncée', - 'pt-br': - 'Direção do movimento quando a tecla Enter é pressionada', - }, - enterMoveDirectionDescription: { - 'en-us': - 'You can move in the opposite direction by pressing Shift+Enter.', - 'ru-ru': - 'Вы можете двигаться в противоположном направлении, нажав Shift+Enter.', - 'es-es': - 'Puedes moverte en la dirección opuesta presionando Shift+Enter.', - 'fr-fr': 'Synonyme couleur.', - 'uk-ua': - 'Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.', - 'de-ch': - 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.', - 'pt-br': - 'Você pode mover na direção oposta pressionando Shift+Enter.', - }, - filterPickLists: { - 'en-us': 'Filter pick list items', - 'ru-ru': 'Фильтрация элементов списка выбора', - 'es-es': 'Filtrar elementos de la lista de selección', - 'fr-fr': 'Filtrer les éléments de la liste de sélection', - 'uk-ua': 'Фільтр вибору елементів списку', - 'de-ch': 'Auswahllistenelemente filtern', - 'pt-br': 'Filtrar itens da lista de seleção', - }, - exportFileDelimiter: { - 'en-us': 'Export file delimiter', - 'ru-ru': 'Разделитель файлов экспорта', - 'es-es': 'Delimitador de archivo de exportación', - 'fr-fr': "Délimiteur de fichier d'exportation", - 'uk-ua': 'Роздільник файлу експорту', - 'de-ch': 'Dateitrennzeichen exportieren', - 'pt-br': 'Delimitador de arquivo de exportação', - }, - exportCsvUtf8Bom: { - 'en-us': 'Add UTF-8 BOM to CSV file exports', - 'ru-ru': 'Добавить UTF-8 BOM в экспорт CSV-файла', - 'es-es': 'Agregar BOM UTF-8 a las exportaciones de archivos CSV', - 'fr-fr': 'Ajouter UTF-8 BOM aux exportations de fichiers CSV', - 'uk-ua': 'Додайте специфікацію UTF-8 до експорту файлу CSVу', - 'de-ch': 'UTF-8 BOM zum CSV-Dateiexport hinzufügen', - 'pt-br': 'Adicionar UTF-8 BOM às exportações de arquivos CSV', - }, - exportCsvUtf8BomDescription: { - 'en-us': - 'Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.', - 'ru-ru': 'Корректное отображение экспортированных CSV-файлов в Excel.', - 'es-es': - 'Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.', - 'fr-fr': - "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", - 'uk-ua': 'Змушує експорт файлів CSV правильно відображатися в Excel.', - 'de-ch': - 'Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.', - 'pt-br': - 'Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.', - }, - caseSensitive: { - 'en-us': 'Case-sensitive', - 'ru-ru': 'С учетом регистра', - 'es-es': 'Distingue mayúsculas y minúsculas', - 'fr-fr': 'Sensible aux majuscules et minuscules', - 'uk-ua': 'Чутливий до регістру', - 'de-ch': 'Groß- und Kleinschreibung beachten', - 'pt-br': 'Maiúsculas e minúsculas', - }, - caseInsensitive: { - 'en-us': 'Case-insensitive', - 'ru-ru': 'Без учета регистра', - 'es-es': 'No distingue entre mayúsculas y minúsculas', - 'fr-fr': 'Insensible à la casse', - 'uk-ua': 'Регістр не враховується', - 'de-ch': 'Groß- und Kleinschreibung wird nicht berücksichtigt', - 'pt-br': 'Não diferencia maiúsculas de minúsculas', - }, - showNoReadTables: { - 'en-us': 'Show tables without "Read" access', - 'ru-ru': 'Показывать таблицы без доступа «Чтение»', - 'es-es': 'Mostrar tablas sin acceso de "Lectura"', - 'fr-fr': 'Afficher les tableaux sans accès "Lecture"', - 'uk-ua': 'Показувати таблиці без доступу «Читання»', - 'de-ch': 'Tabellen ohne Lesezugriff anzeigen', - 'pt-br': 'Mostrar tabelas sem acesso de "Leitura"', - }, - showNoAccessTables: { - 'en-us': 'Show tables without "Create" access', - 'ru-ru': 'Показывать таблицы без права «Создать»', - 'es-es': 'Mostrar tablas sin acceso "Crear"', - 'fr-fr': 'Afficher les tableaux sans accès "Créer"', - 'uk-ua': 'Показувати таблиці без доступу «Створити»', - 'de-ch': 'Tabellen ohne „Erstellen“-Zugriff anzeigen', - 'pt-br': 'Mostrar tabelas sem acesso "Criar"', - }, - textAreaAutoGrow: { - 'en-us': 'Text boxes grow automatically', - 'ru-ru': 'Текстовые поля увеличиваются автоматически', - 'es-es': 'Los cuadros de texto crecen automáticamente', - 'fr-fr': "Les zones de texte s'agrandissent automatiquement", - 'uk-ua': 'Текстові поля збільшуються автоматично', - 'de-ch': 'Textfelder werden automatisch vergrößert', - 'pt-br': 'As caixas de texto crescem automaticamente', - }, - clearQueryFilters: { - 'en-us': 'Reset query filters', - 'ru-ru': 'Сбросить фильтры запроса', - 'es-es': 'Restablecer filtros de consulta', - 'fr-fr': 'Réinitialiser les filtres de requête', - 'uk-ua': 'Скинути фільтри запитів', - 'de-ch': 'Abfragefilter zurücksetzen', - 'pt-br': 'Redefinir filtros de consulta', - }, - clearQueryFiltersDescription: { - 'en-us': 'Clears all query filters when running a Report from a Form.', - 'de-ch': - 'Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.', - 'es-es': - 'Borra todos los filtros de consulta al ejecutar un informe desde un formulario.', - 'fr-fr': - "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", - 'ru-ru': 'Очищает все фильтры запроса при запуске отчета из формы.', - 'uk-ua': 'Очищає всі фільтри запитів під час запуску звіту з форми.', - 'pt-br': - 'Limpa todos os filtros de consulta ao executar um relatório de um formulário.', - }, - queryParamtersFromForm: { - 'en-us': 'Show query filters when running a Report from a Form', - 'de-ch': - 'Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird', - 'es-es': - 'Mostrar filtros de consulta al ejecutar un informe desde un formulario', - 'fr-fr': - "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", - 'ru-ru': 'Показывать фильтры запроса при запуске отчета из формы', - 'uk-ua': 'Показувати фільтри запитів під час запуску звіту з форми', - 'pt-br': - 'Mostrar filtros de consulta ao executar um relatório de um formulário', - }, - autoGrowAutoComplete: { - 'en-us': 'Allow autocomplete to grow as wide as need', - 'ru-ru': - 'Разрешить автозаполнению расширяться настолько, насколько это необходимо', - 'es-es': 'Permitir que el autocompletado crezca tanto como sea necesario', - 'fr-fr': - 'Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée', - 'uk-ua': - 'Дозволити автозаповнення розширюватися настільки, наскільки потрібно', - 'de-ch': - 'Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen', - 'pt-br': - 'Permitir que o preenchimento automático cresça o quanto for necessário', - }, - tableNameInTitle: { - 'en-us': 'Include table name in the browser page title', - 'ru-ru': 'Включить имя таблицы в заголовок страницы браузера', - 'es-es': - 'Incluir el nombre de la tabla en el título de la página del navegador', - 'fr-fr': - 'Inclure le nom de la table dans le titre de la page du navigateur', - 'uk-ua': 'Включіть назву таблиці в заголовок сторінки браузера', - 'de-ch': 'Tabellennamen in den Seitentitel des Browsers aufnehmen', - 'pt-br': 'Incluir nome da tabela no título da página do navegador', - }, - focusFirstField: { - 'en-us': 'Focus first field', - 'de-ch': 'Fokus erstes Feld', - 'es-es': 'Enfoque el primer campo', - 'fr-fr': 'Concentrez-vous sur le premier champ', - 'ru-ru': 'Фокус первого поля', - 'uk-ua': 'Перейти до першого поля', - 'pt-br': 'Foco primeiro campo', - }, - doubleClickZoom: { - 'en-us': 'Double click to zoom', - 'ru-ru': 'Дважды щелкните, чтобы увеличить', - 'es-es': 'Haga doble clic para ampliar', - 'fr-fr': 'Double-cliquez pour zoomer', - 'uk-ua': 'Двічі клацніть, щоб збільшити', - 'de-ch': 'Zum Vergrößern doppelklicken', - 'pt-br': 'Clique duas vezes para ampliar', - }, - closePopupOnClick: { - 'en-us': 'Close pop-up on outside click', - 'ru-ru': 'Закрытие всплывающего окна при внешнем щелчке', - 'es-es': 'Cerrar ventana emergente al hacer clic desde fuera', - 'fr-fr': "Fermer la pop-up lors d'un clic extérieur", - 'uk-ua': 'Закрити спливаюче вікно при зовнішньому клацанні', - 'de-ch': 'Popup bei externem Klick schließen', - 'pt-br': 'Fechar pop-up ao clicar fora', - }, - animateTransitions: { - 'en-us': 'Animate transitions', - 'ru-ru': 'Анимированные переходы', - 'es-es': 'Animar transiciones', - 'fr-fr': 'Animer les transitions', - 'uk-ua': 'Анімація переходів', - 'de-ch': 'Übergänge animieren', - 'pt-br': 'Transições animadas', - }, - panInertia: { - 'en-us': 'Pan inertia', - 'ru-ru': 'Инерция пан', - 'es-es': 'Inercia de la sartén', - 'fr-fr': 'Inertie du bac', - 'uk-ua': 'Інерція панорами', - 'de-ch': 'Schwenkträgheit', - 'pt-br': 'Inércia da panela', - }, - mouseDrags: { - 'en-us': 'Mouse drags', - 'ru-ru': 'Перетаскивание мышью', - 'es-es': 'El ratón arrastra', - 'uk-ua': 'Виділіть відповідний підрядок', - 'de-ch': 'Maus zieht', - 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', - 'pt-br': 'Arrastos do mouse', - }, - scrollWheelZoom: { - 'en-us': 'Scroll wheel zoom', - 'ru-ru': 'Масштабирование с помощью колеса прокрутки', - 'es-es': 'Zoom con rueda de desplazamiento', - 'fr-fr': 'Zoom avec la molette de défilement', - 'uk-ua': 'Масштаб колеса прокрутки', - 'de-ch': 'Scrollrad-Zoom', - 'pt-br': 'Zoom da roda de rolagem', - }, - flexibleColumnWidth: { - 'en-us': 'Flexible column width', - 'ru-ru': 'Гибкая ширина столбца', - 'es-es': 'Ancho de columna flexible', - 'fr-fr': 'Largeur de colonne flexible', - 'uk-ua': 'Гнучка ширина колонки', - 'de-ch': 'Flexible Spaltenbreite', - 'pt-br': 'Largura de coluna flexível', - }, - flexibleSubGridColumnWidth: { - 'en-us': 'Flexible subview grid column width', - 'ru-ru': 'Гибкая ширина столбца сетки подпредставлений', - 'es-es': 'Ancho de columna de cuadrícula de subvista flexible', - 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', - 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', - 'de-ch': 'Flexible Rasterspaltenbreite der Unteransicht', - 'pt-br': 'Largura flexível da coluna da grade de subvisualização', - }, - closeOnEsc: { - 'en-us': 'Close on ESC key press', - 'ru-ru': 'Закрыть нажатием клавиши ESC', - 'es-es': 'Cerrar al presionar la tecla ESC', - 'fr-fr': 'Icône et nom de la table', - 'uk-ua': 'Закриття натисканням клавіші ESC', - 'de-ch': 'Schließen durch Drücken der Taste ESC', - 'pt-br': 'Fechar ao pressionar a tecla ESC', - }, - closeOnOutsideClick: { - 'en-us': 'Close on outside click', - 'ru-ru': 'Закрытие по внешнему щелчку', - 'es-es': 'Cerrar al hacer clic desde fuera', - 'fr-fr': 'Fermer sur clic extérieur', - 'uk-ua': 'Закрийте зовнішнім клацанням', - 'de-ch': 'Schließen durch Klicken von außen', - 'pt-br': 'Fechar com clique externo', - }, - scopeEntireTablePicklists: { - 'en-us': 'Scope "Entire Table" picklists', - }, - scopeEntireTablePicklistsDescription: { - 'en-us': - 'Restrict "Entire Table" picklists to values used by records in this collection.', - }, - catalogNumberInheritanceDescription: { - 'en-us': - 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', - }, - catalogNumberParentInheritanceDescription: { - 'en-us': - 'Control whether component records inherit catalog numbers from their parent Collection Object.', - }, specifyNetworkBadge: { 'en-us': 'Specify Network Badge', 'ru-ru': 'Укажите сетевой значок', @@ -606,100 +79,114 @@ export const preferencesContentStrings = { 'de-ch': 'Netzwerk-Badge angeben', 'pt-br': 'Especificar emblema de rede', }, - useAccessibleFullDatePicker: { - 'en-us': 'Use accessible full date picker', - 'ru-ru': 'Используйте доступный полный выбор даты', - 'es-es': 'Utilice el selector de fecha completo y accesible', - 'fr-fr': 'Utiliser un sélecteur de date complet accessible', - 'uk-ua': 'Використовуйте доступний повний засіб вибору дати', - 'de-ch': 'Verwenden Sie eine barrierefreie Datumsauswahl', - 'pt-br': 'Use o seletor de data completo acessível', - }, - useAccessibleMonthPicker: { - 'en-us': 'Use accessible month picker', - 'ru-ru': 'Используйте доступный выбор месяца', - 'es-es': 'Utilice el selector de meses accesible', - 'fr-fr': 'Utiliser le sélecteur de mois accessible', - 'uk-ua': 'Використовуйте доступний засіб вибору місяця', - 'de-ch': 'Verwenden Sie die barrierefreie Monatsauswahl', - 'pt-br': 'Use o seletor de meses acessível', - }, - rightAlignNumberFields: { - 'en-us': 'Right-Justify numeric fields', - 'ru-ru': 'Выравнивание числовых полей по правому краю', - 'es-es': 'Justificar a la derecha los campos numéricos', - 'fr-fr': 'Justifier à droite les champs numériques', - 'uk-ua': 'Вирівнювання по правому краю числових полів', - 'de-ch': 'Rechtsbündige Ausrichtung numerischer Felder', - 'pt-br': 'Justificar à direita campos numéricos', - }, - roundedCorners: { - 'en-us': 'Rounded corners', - 'ru-ru': 'Закругленные углы', - 'es-es': 'esquinas redondeadas', - 'fr-fr': 'Coins arrondis', - 'uk-ua': 'Заокруглені кути', - 'de-ch': 'Abgerundete Ecken', - 'pt-br': 'Cantos arredondados', - }, - showSubviewBorders: { - 'en-us': 'Show borders around subviews', - 'de-ch': 'Rahmen um Unteransichten anzeigen', - 'es-es': 'Mostrar bordes alrededor de las subvistas', - 'fr-fr': 'Afficher les bordures autour des sous-vues', - 'pt-br': 'Mostrar bordas ao redor das subvisualizações', - 'ru-ru': 'Показывать границы вокруг подпредставлений', - 'uk-ua': 'Показати межі навколо підвидів', - }, - limitMaxFieldWidth: { - 'en-us': 'Limit max field width', - 'ru-ru': 'Ограничить максимальную ширину поля', - 'es-es': 'Limitar el ancho máximo del campo', - 'fr-fr': 'Limiter la largeur maximale du champ', - 'uk-ua': 'Обмеження максимальної ширини поля', - 'de-ch': 'Maximale Feldbreite begrenzen', - 'pt-br': 'Limite a largura máxima do campo', - }, - condenseQueryResults: { - 'en-us': 'Condense query results', - 'ru-ru': 'Сжать результаты запроса', - 'es-es': 'Condensar los resultados de la consulta', - 'fr-fr': 'Condenser les résultats de la requête', - 'uk-ua': 'Згорнути результати запиту', - 'de-ch': 'Abfrageergebnisse verdichten', - 'pt-br': 'Condensar resultados da consulta', - }, - blurContentBehindDialog: { - 'en-us': 'Blur content behind the dialog', - 'ru-ru': 'Размытие содержимого за диалогом', - 'es-es': 'Desenfocar el contenido detrás del diálogo', - 'fr-fr': 'Flou le contenu derrière la boîte de dialogue', - 'uk-ua': 'Розмити вміст за діалоговим вікном', - 'de-ch': 'Inhalte hinter dem Dialog verwischen', - 'pt-br': 'Desfocar o conteúdo atrás do diálogo', - }, - collectionSortOrderDescription: { - 'en-us': 'This determines the visual order of collections.', - 'ru-ru': 'Это определяет визуальный порядок коллекций.', - 'es-es': 'Esto determina el orden visual de las colecciones.', - 'fr-fr': "Ceci détermine l'ordre visuel des collections.", - 'uk-ua': 'Це визначає візуальний порядок колекцій.', - 'de-ch': 'Dies bestimmt die visuelle Reihenfolge der Sammlungen.', - 'pt-br': 'Isso determina a ordem visual das coleções.', - }, - recordSetRecordToOpen: { - 'en-us': 'Record to open by default', - 'ru-ru': 'Запись для открытия по умолчанию', - 'es-es': 'Registro para abrir por defecto', - 'fr-fr': 'Enregistrement à ouvrir par défaut', - 'uk-ua': 'Запис відкривається за умовчанням', - 'de-ch': 'Standardmäßig zu öffnender Datensatz', - 'pt-br': 'Gravar para abrir por padrão', + url: { + 'en-us': 'URL', + 'de-ch': 'URL', + 'es-es': 'URL', + 'fr-fr': 'URL', + 'uk-ua': 'URL', + 'ru-ru': 'URL', + 'pt-br': 'URL', + }, + pickAttachment: { + 'en-us': 'Pick an attachment', + 'es-es': 'Elige un archivo adjunto', + 'fr-fr': 'Choisissez une pièce jointe', + 'ru-ru': 'Выберите вложение', + 'uk-ua': 'Виберіть вкладення', + 'de-ch': 'Wählen Sie einen Anhang', + 'pt-br': 'Escolha um anexo', + }, + attachmentFailed: { + 'en-us': 'The attachment failed to load.', + 'de-ch': 'Der Anhang konnte nicht geladen werden.', + 'es-es': 'No se pudo cargar el archivo adjunto.', + 'fr-fr': "La pièce jointe n'a pas pu être chargée.", + 'ru-ru': 'Не удалось загрузить вложение.', + 'uk-ua': 'Не вдалося завантажити вкладений файл.', + 'pt-br': 'O anexo não pôde ser carregado.', + }, + pickImage: { + 'en-us': 'Pick an image', + 'de-ch': 'Wählen Sie ein Bild aus', + 'es-es': 'Elige una imagen', + 'fr-fr': 'Choisissez une image', + 'ru-ru': 'Выберите изображение', + 'uk-ua': 'Виберіть зображення', + 'pt-br': 'Escolha uma imagem', + }, + customLogo: { + 'en-us': 'Expanded Image URL', + 'de-ch': 'Erweiterte Bild-URL', + 'es-es': 'URL de imagen expandida', + 'fr-fr': "URL de l'image étendue", + 'ru-ru': 'URL-адрес развернутого изображения', + 'uk-ua': 'Розширена URL-адреса зображення', + 'pt-br': 'URL da imagem expandida', + }, + customLogoCollapsed: { + 'en-us': 'Collapsed Image URL', + 'de-ch': 'URL des minimierten Bildes', + 'es-es': 'URL de imagen contraída', + 'fr-fr': "URL de l'image réduite", + 'ru-ru': 'URL-адрес свернутого изображения', + 'uk-ua': 'URL-адреса згорнутого зображення', + 'pt-br': 'URL da imagem recolhida', + }, + customLogoDescription: { + 'en-us': + 'A URL to an image that would be displayed next to the Specify logo in the navigation menu.', + 'de-ch': + 'Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.', + 'es-es': + 'Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.', + 'fr-fr': + 'Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.', + 'ru-ru': + 'URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.', + 'uk-ua': + 'URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.', + 'pt-br': + 'Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.', + }, + attachmentPreviewMode: { + 'en-us': 'Attachment preview mode', + 'de-ch': 'Anhangsvorschaumodus', + 'es-es': 'Modo de vista previa de archivos adjuntos', + 'fr-fr': "Mode d'aperçu des pièces jointes", + 'ru-ru': 'Режим предварительного просмотра вложений', + 'uk-ua': 'Режим попереднього перегляду вкладених файлів', + 'pt-br': 'Modo de visualização de anexos', + }, + fullResolution: { + 'en-us': 'Full Resolution', + 'de-ch': 'Volle Auflösung', + 'es-es': 'Resolución completa', + 'fr-fr': 'Pleine résolution', + 'ru-ru': 'Полное разрешение', + 'uk-ua': 'Повна роздільна здатність', + 'pt-br': 'Resolução completa', + }, + thumbnail: { + 'en-us': 'Thumbnail', + 'de-ch': 'Miniaturansicht', + 'es-es': 'Uña del pulgar', + 'fr-fr': 'Vignette', + 'ru-ru': 'Миниатюра', + 'uk-ua': 'Мініатюра', + 'pt-br': 'Miniatura', + }, + addSearchBarHomePage: { + 'en-us': 'Add Search Bar on home page', + 'de-ch': 'Suchleiste auf der Startseite hinzufügen', + 'es-es': 'Agregar barra de búsqueda en la página de inicio', + 'fr-fr': "Ajouter une barre de recherche sur la page d'accueil", + 'ru-ru': 'Добавить панель поиска на домашнюю страницу', + 'uk-ua': 'Додайте рядок пошуку на головну сторінку', + 'pt-br': 'Adicionar barra de pesquisa na página inicial', }, } as const; export const preferencesContentText = createDictionary( - preferencesContentStrings + preferencesContentDictionary ); - -export default preferencesContentText; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.general.ts b/specifyweb/frontend/js_src/lib/localization/preferences.general.ts new file mode 100644 index 00000000000..30d8adf9225 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/localization/preferences.general.ts @@ -0,0 +1,2092 @@ +/** + * Localization strings for general preferences. + * + * @module + */ +import { createDictionary } from './utils'; +// Refer to "Guidelines for Programmers" in ./README.md before editing this file +export const preferencesGeneralDictionary = { + preferences: { + "en-us": "Preferences", + "ru-ru": "Настройки", + "es-es": "Preferencias", + "fr-fr": "Préférences", + "uk-ua": "Уподобання", + "de-ch": "Einstellungen", + "pt-br": "Preferências", + }, + customization: { + "en-us": "Customization", + "ru-ru": "Настройка", + "es-es": "Personalización", + "fr-fr": "Personnalisation", + "uk-ua": "Спеціальнізація", + "de-ch": "Anpassung", + "pt-br": "Personalização", + }, + userPreferences: { + "en-us": "User Preferences", + "ru-ru": "Настройки пользователя", + "es-es": "Preferencias del usuario", + "fr-fr": "Préférences de l'utilisateur", + "uk-ua": "Налаштування користувача", + "de-ch": "Benutzereinstellungen", + "pt-br": "Preferências do usuário", + }, + defaultUserPreferences: { + "en-us": "Default User Preferences", + "ru-ru": "Настройки пользователя по умолчанию", + "es-es": "Preferencias de usuario predeterminadas", + "fr-fr": "Préférences utilisateur par défaut", + "uk-ua": "Параметри користувача за умовчанням", + "de-ch": "Standardbenutzereinstellungen", + "pt-br": "Preferências de usuário padrão", + }, + general: { + "en-us": "General", + "ru-ru": "Общий", + "es-es": "General", + "fr-fr": "Image personnalisée", + "uk-ua": "Спеціальне зображення", + "de-ch": "Allgemein", + "pt-br": "Em geral", + }, + ui: { + "en-us": "User Interface", + "ru-ru": "Пользовательский интерфейс", + "es-es": "Interfaz de usuario", + "fr-fr": "Interface utilisateur", + "uk-ua": "Інтерфейс користувача", + "de-ch": "Benutzeroberfläche", + "pt-br": "Interface do usuário", + }, + theme: { + "en-us": "Theme", + "ru-ru": "Тема", + "es-es": "Tema", + "fr-fr": "Thème", + "uk-ua": "Тема", + "de-ch": "Thema", + "pt-br": "Tema", + }, + useSystemSetting: { + "en-us": "Use system setting", + "ru-ru": "Использовать системные настройки", + "es-es": "Utilizar la configuración del sistema", + "fr-fr": "Utiliser les paramètres du système", + "uk-ua": "Використовуйте налаштування системи", + "de-ch": "Systemeinstellung verwenden", + "pt-br": "Usar configuração do sistema", + }, + inheritOsSettings: { + "en-us": "Copies value from your Operating System settings", + "ru-ru": "Копирует значение из настроек вашей операционной системы", + "es-es": "Copia el valor de la configuración de su sistema operativo", + "fr-fr": "Copie la valeur des paramètres de votre système d'exploitation", + "uk-ua": "Копіює значення з налаштувань вашої операційної системи", + "de-ch": "Übernimmt den Wert aus Ihren Betriebssystemeinstellungen", + "pt-br": "Copia o valor das configurações do seu sistema operacional", + }, + light: { + comment: "Light mode", + "en-us": "Light", + "ru-ru": "Свет", + "es-es": "Luz", + "fr-fr": "Lumière", + "uk-ua": "світло", + "de-ch": "Hell", + "pt-br": "Luz", + }, + dark: { + comment: "Dark mode", + "en-us": "Dark", + "ru-ru": "Темный", + "es-es": "Oscuro", + "fr-fr": "Sombre", + "uk-ua": "Темний", + "de-ch": "Dunkel", + "pt-br": "Escuro", + }, + reduceMotion: { + "en-us": "Reduce motion", + "ru-ru": "Уменьшите движение", + "es-es": "Reducir el movimiento", + "fr-fr": "Réduire les mouvements", + "uk-ua": "Зменшити рух", + "de-ch": "Bewegung reduzieren", + "pt-br": "Reduzir movimento", + }, + reduceMotionDescription: { + "en-us": "Disable non-essential animations and transitions.", + "ru-ru": "Отключите ненужные анимации и переходы.", + "es-es": "Desactivar animaciones y transiciones no esenciales.", + "fr-fr": "Désactivez les animations et les transitions non essentielles.", + "uk-ua": "Вимкніть необов'язкову анімацію та переходи.", + "de-ch": "Nicht erforderliche Animationen und Übergänge deaktivieren.", + "pt-br": "Desabilite animações e transições não essenciais.", + }, + reduceTransparency: { + "en-us": "Reduce transparency", + "ru-ru": "Уменьшить прозрачность", + "es-es": "Reducir la transparencia", + "fr-fr": "Réduire la transparence", + "uk-ua": "Зменшити прозорість", + "de-ch": "Transparenz reduzieren", + "pt-br": "Reduzir a transparência", + }, + reduceTransparencyDescription: { + "en-us": + "Whether to disable translucent backgrounds for user interface components whenever possible (e.g. table headers in tree view).", + "ru-ru": + "Следует ли отключать полупрозрачный фон для компонентов пользовательского интерфейса, когда это возможно (например, заголовки таблиц в древовидной структуре).", + "es-es": + "Si se deben deshabilitar los fondos translúcidos para los componentes de la interfaz de usuario siempre que sea posible (por ejemplo, encabezados de tabla en la vista de árbol).", + "fr-fr": + "S'il faut désactiver les arrière-plans translucides pour les composants de l'interface utilisateur chaque fois que possible (par exemple, les en-têtes de tableau dans l'arborescence).", + "uk-ua": + "Чи вимикати напівпрозорий фон для компонентів інтерфейсу користувача, коли це можливо (наприклад, заголовки таблиць у перегляді дерева).", + "de-ch": + "Durchsichtige Hintergründe für Benutzeroberflächenkomponenten wann immer möglich deaktivieren (z. B. Tabellenüberschriften in der Baumansicht).", + "pt-br": + "Se deve ou não desabilitar fundos translúcidos para componentes da interface do usuário sempre que possível (por exemplo, cabeçalhos de tabela na visualização em árvore).", + }, + contrast: { + "en-us": "Contrast", + "ru-ru": "Контраст", + "es-es": "Contraste", + "fr-fr": "Contraste", + "uk-ua": "Контраст", + "de-ch": "Kontrast", + "pt-br": "Contraste", + }, + increase: { + "en-us": "Increase", + "ru-ru": "Увеличивать", + "es-es": "Aumentar", + "fr-fr": "Augmenter", + "uk-ua": "Збільшити", + "de-ch": "Erhöhen", + "pt-br": "Aumentar", + }, + reduce: { + "en-us": "Reduce", + "ru-ru": "Уменьшать", + "es-es": "Reducir", + "fr-fr": "Réduire", + "uk-ua": "Зменшити", + "de-ch": "Verringern", + "pt-br": "Reduzir", + }, + noPreference: { + "en-us": "No preference", + "ru-ru": "Нет предпочтений", + "es-es": "Sin preferencia", + "fr-fr": "Pas de préférence", + "uk-ua": "Без переваг", + "de-ch": "Keine Präferenz", + "pt-br": "Sem preferência", + }, + fontSize: { + "en-us": "Font size", + "ru-ru": "Размер шрифта", + "es-es": "Tamaño de fuente", + "fr-fr": "Taille de police", + "uk-ua": "Розмір шрифту", + "de-ch": "Schriftgrösse", + "pt-br": "Tamanho da fonte", + }, + fontFamily: { + "en-us": "Font family", + "ru-ru": "Семейство шрифтов", + "es-es": "Familia de fuentes", + "fr-fr": "Famille de polices", + "uk-ua": "Сімейство шрифтів", + "de-ch": "Schrift-Familie", + "pt-br": "Família de fontes", + }, + fontFamilyDescription: { + "en-us": + "You can specify any font that is on your computer, even if it is not in the list. A comma-separated list of fonts is also supported, where each subsequent font will be used if the previous one is not available.", + "ru-ru": + "Вы можете указать любой шрифт, установленный на вашем компьютере, даже если его нет в списке. Также поддерживается список шрифтов, разделённый запятыми, где каждый последующий шрифт будет использоваться, если предыдущий недоступен.", + "es-es": + "Puede especificar cualquier fuente de su ordenador, incluso si no está en la lista. También se admite una lista de fuentes separadas por comas, donde se usará cada fuente subsiguiente si la anterior no está disponible.", + "fr-fr": + "Vous pouvez spécifier n'importe quelle police présente sur votre ordinateur, même si elle ne figure pas dans la liste. Une liste de polices séparées par des virgules est également prise en charge ; chaque police suivante sera utilisée si la précédente n'est pas disponible.", + "uk-ua": + "Ви можете вказати будь-який шрифт, який є на вашому комп'ютері, навіть якщо його немає в списку. Також підтримується розділений комами список шрифтів, у якому використовуватиметься другий шрифт, якщо перший недоступний тощо.", + "de-ch": + "Sie können jede Schriftart angeben, die sich auf Ihrem Computer befindet, auch wenn diese nicht in der Liste enthalten ist. Eine durch Kommas getrennte Liste von Schriftarten wird ebenfalls unterstützt, wobei die zweite Schriftart verwendet wird, wenn die erste nicht verfügbar ist usw.", + "pt-br": + "Você pode especificar qualquer fonte que esteja no seu computador, mesmo que ela não esteja na lista. Uma lista de fontes separadas por vírgulas também é suportada, onde cada fonte subsequente será usada se a anterior não estiver disponível.", + }, + defaultFont: { + "en-us": "(default font)", + "ru-ru": "(шрифт по умолчанию)", + "es-es": "(fuente predeterminada)", + "fr-fr": "(police par défaut)", + "uk-ua": "(типовий шрифт)", + "de-ch": "(Standardschriftart)", + "pt-br": "(fonte padrão)", + }, + maxFormWidth: { + "en-us": "Max form width", + "ru-ru": "Максимальная ширина формы", + "es-es": "Ancho máximo del formulario", + "fr-fr": "Largeur maximale du formulaire", + "uk-ua": "Максимальна ширина форми", + "de-ch": "Maximale Formularbreite", + "pt-br": "Largura máxima do formulário", + }, + fieldBackgrounds: { + "en-us": "Field backgrounds", + "ru-ru": "Фоны полей", + "es-es": "Fondos de campo", + "fr-fr": "Milieux de terrain", + "uk-ua": "Польові фони", + "de-ch": "Feldhintergründe", + "pt-br": "Fundos de campo", + }, + fieldBackground: { + "en-us": "Field background", + "ru-ru": "Фон поля", + "es-es": "Fondo de campo", + "fr-fr": "Contexte du terrain", + "uk-ua": "Поле фону", + "de-ch": "Feldhintergrund", + "pt-br": "Contexto de campo", + }, + disabledFieldBackground: { + "en-us": "Disabled field background", + "ru-ru": "Отключенный фон поля", + "es-es": "Fondo de campo deshabilitado", + "fr-fr": "Fond de champ désactivé", + "uk-ua": "Вимкнений фон поля", + "de-ch": "Deaktivierter Feldhintergrund", + "pt-br": "Fundo de campo desativado", + }, + invalidFieldBackground: { + "en-us": "Invalid field background", + "ru-ru": "Неверный фон поля", + "es-es": "Fondo de campo no válido", + "fr-fr": "Fond de champ invalide", + "uk-ua": "Недійсний фон поля", + "de-ch": "Ungültiger Feldhintergrund", + "pt-br": "Fundo de campo inválido", + }, + requiredFieldBackground: { + "en-us": "Required field background", + "ru-ru": "Обязательное поле фон", + "es-es": "Antecedentes del campo obligatorio", + "fr-fr": "Contexte du champ obligatoire", + "uk-ua": "Обов'язковий фон поля", + "de-ch": "Feldhintergrund erforderlich", + "pt-br": "Histórico de campo obrigatório", + }, + darkFieldBackground: { + "en-us": "Field background (dark theme)", + "ru-ru": "Фон поля (тёмная тема)", + "es-es": "Fondo de campo (tema oscuro)", + "fr-fr": "Fond de champ (thème sombre)", + "uk-ua": "Фон поля (темна тема)", + "de-ch": "Feldhintergrund (Dunkles Thema)", + "pt-br": "Fundo de campo (tema escuro)", + }, + darkDisabledFieldBackground: { + "en-us": "Disabled field background (dark theme)", + "ru-ru": "Отключенный фон поля (тёмная тема)", + "es-es": "Fondo de campo deshabilitado (tema oscuro)", + "fr-fr": "Fond de champ désactivé (thème sombre)", + "uk-ua": "Вимкнений фон поля (темна тема)", + "de-ch": "Deaktivierter Feldhintergrund (Dunkles Thema)", + "pt-br": "Fundo de campo desativado (tema escuro)", + }, + darkInvalidFieldBackground: { + "en-us": "Invalid field background (dark theme)", + "ru-ru": "Недопустимый фон поля (тёмная тема)", + "es-es": "Fondo de campo no válido (tema oscuro)", + "fr-fr": "Largeur de colonne de grille de sous-vue flexible", + "uk-ua": "Гнучка ширина стовпця сітки вкладеного перегляду", + "de-ch": "Ungültiger Feldhintergrund (Dunkles Thema)", + "pt-br": "Fundo de campo inválido (tema escuro)", + }, + darkRequiredFieldBackground: { + "en-us": "Required field background (dark theme)", + "ru-ru": "Обязательное поле фон (тёмная тема)", + "es-es": "Fondo del campo obligatorio (tema oscuro)", + "fr-fr": "Fond de champ obligatoire (thème sombre)", + "uk-ua": "Обов’язковий фон поля (темна тема)", + "de-ch": "Feldhintergrund erforderlich (Dunkles Thema)", + "pt-br": "Fundo de campo obrigatório (tema escuro)", + }, + dialogs: { + "en-us": "Dialogs", + "ru-ru": "Диалоги", + "es-es": "Diálogos", + "fr-fr": "Boîtes de dialogue", + "uk-ua": "Діалоги", + "de-ch": "Dialoge", + "pt-br": "Diálogos", + }, + appearance: { + "en-us": "Appearance", + "ru-ru": "Появление", + "es-es": "Apariencia", + "fr-fr": "Apparence", + "uk-ua": "Зовнішній вигляд", + "de-ch": "Aussehen", + "pt-br": "Aparência", + }, + buttonsLight: { + "en-us": "Buttons (light mode)", + "de-ch": "Buttons (Helles Thema)", + "es-es": "Botones (modo luz)", + "fr-fr": "Boutons (mode lumière)", + "ru-ru": "Кнопки (световой режим)", + "uk-ua": "Кнопки (світлий режим)", + "pt-br": "Botões (modo claro)", + }, + buttonsDark: { + "en-us": "Buttons (dark mode)", + "de-ch": "Buttons (Dunkles Thema)", + "es-es": "Botones (modo oscuro)", + "fr-fr": "Boutons (mode sombre)", + "ru-ru": "Кнопки (темный режим)", + "uk-ua": "Кнопки (темний режим)", + "pt-br": "Botões (modo escuro)", + }, + translucentDialog: { + "en-us": "Translucent dialogs", + "ru-ru": "Прозрачные диалоги", + "es-es": "Diálogos translúcidos", + "fr-fr": "Dialogues translucides", + "uk-ua": "Напівпрозорі діалоги", + "de-ch": "Durchscheinende Dialoge", + "pt-br": "Diálogos translúcidos", + }, + translucentDialogDescription: { + "en-us": "Whether dialogs have translucent background.", + "ru-ru": "Имеют ли диалоговые окна полупрозрачный фон.", + "es-es": "Si los diálogos tienen fondo translúcido.", + "fr-fr": "Si les boîtes de dialogue ont un fond translucide.", + "uk-ua": "Чи мають діалоги прозорий фон.", + "de-ch": "Dialogfenster mit durchscheinenden Hintergrund.", + "pt-br": "Se os diálogos têm fundo translúcido.", + }, + alwaysPrompt: { + "en-us": "Always prompt to choose collection", + "ru-ru": "Всегда предлагайте выбрать коллекцию", + "es-es": "Siempre dispuesto a elegir la colección", + "fr-fr": "Toujours invité à choisir la collection", + "uk-ua": "Завжди підкажуть вибрати колекцію", + "de-ch": "Immer zur Auswahl der Sammlung auffordern", + "pt-br": "Sempre pronto para escolher a coleção", + }, + treeEditor: { + "en-us": "Tree Editor", + "ru-ru": "Редактор деревьев", + "es-es": "Editor de árboles", + "fr-fr": "Éditeur d'arborescence", + "uk-ua": "Редактор дерева", + "de-ch": "Baumeditor", + "pt-br": "Editor de Árvore", + }, + treeAccentColor: { + "en-us": "Tree accent color", + "ru-ru": "Акцентный цвет дерева", + "es-es": "Color de acento del árbol", + "fr-fr": "Couleur d'accent d'arbre", + "uk-ua": "Колір акценту дерева", + "de-ch": "Baumakzentfarbe", + "pt-br": "Cor de destaque da árvore", + }, + synonymColor: { + "en-us": "Synonym color", + "ru-ru": "Синоним цвет", + "es-es": "Color sinónimo", + "fr-fr": "Synonyme couleur", + "uk-ua": "Синонім кольору", + "de-ch": "Synonymfarbe", + "pt-br": "Cor sinônimo", + }, + showNewDataSetWarning: { + "en-us": "Show new Data Set warning", + "ru-ru": "Показать предупреждение о новом наборе данных", + "es-es": "Mostrar nueva advertencia de conjunto de datos", + "fr-fr": "Afficher un nouvel avertissement sur l'ensemble de données", + "uk-ua": "Показати попередження про новий набір даних", + "de-ch": "Warnung für neuen Datensatz anzeigen", + "pt-br": "Mostrar novo aviso de conjunto de dados", + }, + showNewDataSetWarningDescription: { + "en-us": "Show an informational message when creating a new Data Set.", + "ru-ru": + "Показывать информационное сообщение при создании нового набора данных.", + "es-es": + "Mostrar un mensaje informativo al crear un nuevo conjunto de datos.", + "fr-fr": + "Afficher un message d'information lors de la création d'un nouvel ensemble de données.", + "uk-ua": + "Показувати інформаційне повідомлення під час створення нового набору даних.", + "de-ch": "Zeige eine Meldung beim erstellen eines neuen Datensatzes an.", + "pt-br": + "Exibir uma mensagem informativa ao criar um novo conjunto de dados.", + }, + header: { + "en-us": "Navigation Menu", + "ru-ru": "Меню навигации", + "es-es": "Menú de navegación", + "fr-fr": "le menu de navigation", + "uk-ua": "Навігаційне меню", + "de-ch": "Navigationsmenü", + "pt-br": "Menu de navegação", + }, + application: { + "en-us": "Application", + "ru-ru": "Приложение", + "es-es": "Solicitud", + "fr-fr": "Application", + "uk-ua": "застосування", + "de-ch": "Anwendung", + "pt-br": "Aplicativo", + }, + allowDismissingErrors: { + "en-us": "Allow dismissing error messages", + "ru-ru": "Разрешить отклонять сообщения об ошибках", + "es-es": "Permitir descartar mensajes de error", + "fr-fr": "Autoriser le rejet des messages d'erreur", + "uk-ua": "Дозволити закривати повідомлення про помилки", + "de-ch": "Erlaube das Verwerfen von Fehlermeldungen", + "pt-br": "Permitir descartar mensagens de erro", + }, + updatePageTitle: { + "en-us": "Update page title", + "ru-ru": "Обновить заголовок страницы", + "es-es": "Actualizar el título de la página", + "fr-fr": "Mettre à jour le titre de la page", + "uk-ua": "Оновити назву сторінки", + "de-ch": "Seitentitel aktualisieren", + "pt-br": "Atualizar título da página", + }, + updatePageTitleDescription: { + "en-us": + "Whether to update the title of the page to match dialog's header.", + "ru-ru": + "Обновлять ли заголовок страницы в соответствии с заголовком диалогового окна.", + "es-es": + "Si se debe actualizar el título de la página para que coincida con el encabezado del cuadro de diálogo.", + "fr-fr": + "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'en-tête de la boîte de dialogue.", + "uk-ua": + "Чи оновлювати назву сторінки відповідно до заголовка діалогового вікна.", + "de-ch": + "Titel der Seite so aktualisieren, dass er mit der Kopfzeile des Dialogs übereinstimmt.", + "pt-br": + "Se o título da página deve ser atualizado para corresponder ao cabeçalho da caixa de diálogo.", + }, + updatePageTitleFormDescription: { + "en-us": "Whether to update the title of the page to match current record.", + "ru-ru": + "Следует ли обновить заголовок страницы в соответствии с текущей записью.", + "es-es": + "Si desea actualizar el título de la página para que coincida con el registro actual.", + "fr-fr": + "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'enregistrement actuel.", + "uk-ua": "Чи оновлювати назву сторінки відповідно до поточного запису.", + "de-ch": + "Titel der Seite aktualisieren, damit er mit dem aktuellen Datensatz übereinstimmt.", + "pt-br": + "Se o título da página deve ser atualizado para corresponder ao registro atual.", + }, + queryComboBox: { + "en-us": "Query Combo Box", + "ru-ru": "Поле со списком запросов", + "es-es": "Cuadro combinado de consulta", + "uk-ua": "Поле зі списком запитів", + "de-ch": "Abfrage-Kombinationsfeld", + "fr-fr": "Zone de liste déroulante de requête", + "pt-br": "Caixa de combinação de consulta", + }, + searchAlgorithm: { + "en-us": "Search Algorithm", + "ru-ru": "Алгоритм поиска", + "es-es": "Algoritmo de búsqueda", + "fr-fr": "Algorithme de recherche", + "uk-ua": "Алгоритм пошуку", + "de-ch": "Suchalgorithmus", + "pt-br": "Algoritmo de Busca", + }, + treeSearchAlgorithm: { + "en-us": "Search Algorithm (for relationships with tree tables)", + "ru-ru": "Алгоритм поиска (для связей с древовидными таблицами)", + "es-es": "Algoritmo de búsqueda (para relaciones con tablas de árbol)", + "fr-fr": + "Algorithme de recherche (pour les relations avec les tables arborescentes)", + "uk-ua": "Алгоритм пошуку (для зв’язків із деревоподібними таблицями)", + "de-ch": "Suchalgorithmus (für Beziehungen mit Baumtabellen)", + "pt-br": "Algoritmo de busca (para relacionamentos com tabelas de árvore)", + }, + startsWithInsensitive: { + "en-us": "Starts With (case-insensitive)", + "ru-ru": "Начинается с (без учета регистра)", + "es-es": "Comienza con (sin distinguir entre mayúsculas y minúsculas)", + "fr-fr": "Commence par (insensible à la casse)", + "uk-ua": "Починається з (без урахування регістру)", + "de-ch": "Beginnt mit (Groß-/Kleinschreibung wird nicht beachtet)", + "pt-br": "Começa com (sem distinção de maiúsculas e minúsculas)", + }, + startsWithDescription: { + "en-us": "Search for values that begin with a given query string.", + "ru-ru": "Поиск значений, начинающихся с заданной строки запроса.", + "es-es": + "Busque valores que comiencen con una cadena de consulta determinada.", + "fr-fr": + "Rechercher des valeurs commençant par une chaîne de requête donnée.", + "uk-ua": "Пошук значень, які починаються з заданого рядка запиту.", + "de-ch": + "Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.", + "pt-br": + "Pesquisar valores que começam com uma determinada sequência de consulta.", + }, + startsWithCaseSensitive: { + "en-us": "Starts With (case-sensitive)", + "ru-ru": "Начинается с (с учетом регистра)", + "es-es": "Comienza con (sensible a mayúsculas y minúsculas)", + "fr-fr": "Commence par (sensible à la casse)", + "uk-ua": "Починається з (з урахуванням регістру)", + "de-ch": "Beginnt mit (Groß-/Kleinschreibung beachten)", + "pt-br": "Começa com (diferencia maiúsculas de minúsculas)", + }, + startsWithCaseSensitiveDescription: { + "en-us": "Search for values that begin with a given query string.", + "ru-ru": "Поиск значений, начинающихся с заданной строки запроса.", + "es-es": + "Busque valores que comiencen con una cadena de consulta determinada.", + "fr-fr": + "Recherchez les valeurs qui commencent par une chaîne de requête donnée.", + "uk-ua": "Пошук значень, які починаються з заданого рядка запиту.", + "de-ch": + "Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.", + "pt-br": + "Pesquisar valores que começam com uma determinada sequência de consulta.", + }, + containsInsensitive: { + "en-us": "Contains (case-insensitive)", + "ru-ru": "Содержит (без учета регистра)", + "es-es": "Contiene (sin distinguir entre mayúsculas y minúsculas)", + "fr-fr": "Contient (insensible à la casse)", + "uk-ua": "Містить (незалежно від регістру)", + "de-ch": "Enthält (Groß-/Kleinschreibung wird nicht beachtet)", + "pt-br": "Contém (sem distinção entre maiúsculas e minúsculas)", + }, + containsCaseSensitive: { + "en-us": "Contains (case-sensitive)", + "ru-ru": "Содержит (с учетом регистра)", + "es-es": "Contiene (sensible a mayúsculas y minúsculas)", + "fr-fr": "Contient (sensible à la casse)", + "uk-ua": "Містить (з урахуванням регістру)", + "de-ch": "Enthält (Groß-/Kleinschreibung beachten)", + "pt-br": "Contém (diferencia maiúsculas de minúsculas)", + }, + containsDescription: { + "en-us": + "Search for values that contain a given query string (case-insensitive).", + "ru-ru": + "Поиск значений, содержащих заданную строку запроса (без учета регистра).", + "es-es": + "Busque valores que contengan una cadena de consulta determinada (sin distinguir entre mayúsculas y minúsculas).", + "uk-ua": + "Пошук значень, які містять заданий рядок запиту (незалежно від регістру).", + "de-ch": + "Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (ohne Berücksichtigung der Groß-/Kleinschreibung).", + "fr-fr": + "Recherchez les valeurs contenant une chaîne de requête donnée (insensible à la casse).", + "pt-br": + "Pesquisar valores que contenham uma determinada sequência de consulta (sem distinção de maiúsculas e minúsculas).", + }, + containsCaseSensitiveDescription: { + "en-us": + "Search for values that contain a given query string (case-sensitive).", + "ru-ru": + "Поиск значений, содержащих заданную строку запроса (с учетом регистра).", + "es-es": + "Busque valores que contengan una cadena de consulta determinada (distingue entre mayúsculas y minúsculas).", + "fr-fr": + "Recherchez les valeurs contenant une chaîne de requête donnée (sensible à la casse).", + "uk-ua": + "Пошук значень, які містять заданий рядок запиту (з урахуванням регістру).", + "de-ch": + "Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (Groß-/Kleinschreibung beachten).", + "pt-br": + "Pesquisar valores que contenham uma determinada sequência de consulta (diferencia maiúsculas de minúsculas).", + }, + containsSecondDescription: { + "en-us": + "Can use _ to match any single character or % to match any number of characters.", + "ru-ru": + "Можно использовать _ для соответствия любому отдельному символу или % для соответствия любому количеству символов.", + "es-es": + "Puede utilizar _ para que coincida con cualquier carácter individual o % para que coincida con cualquier número de caracteres.", + "fr-fr": + "Peut utiliser _ pour correspondre à n'importe quel caractère ou % pour correspondre à n'importe quel nombre de caractères.", + "uk-ua": + "Можна використовувати _ для відповідності будь-якому одному символу або % для відповідності будь-якій кількості символів.", + "de-ch": + "Sie können _ verwenden, um ein beliebiges einzelnes Zeichen abzugleichen, oder %, um eine beliebige Anzahl von Zeichen abzugleichen.", + "pt-br": + "Pode usar _ para corresponder a qualquer caractere único ou % para corresponder a qualquer número de caracteres.", + }, + highlightMatch: { + "en-us": "Highlight matched substring", + "ru-ru": "Выделить совпавшую подстроку", + "es-es": "Resaltar la subcadena coincidente", + "fr-fr": "Mettre en surbrillance la sous-chaîne correspondante", + "uk-ua": "Виділіть збіг підрядка", + "de-ch": "Markieren Sie übereinstimmende Teilzeichenfolgen", + "pt-br": "Destacar substring correspondente", + }, + languageDescription: { + "en-us": "Determines field captions, usage notes and table captions.", + "ru-ru": + "Определяет заголовки полей, примечания по использованию и заголовки таблиц.", + "es-es": "Determina títulos de campos, notas de uso y títulos de tablas.", + "fr-fr": + "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux.", + "uk-ua": + "Визначає підписи полів, примітки щодо використання та підписи таблиць.", + "de-ch": + "Legt Feldbeschriftungen, Verwendungshinweise und Tabellenbeschriftungen fest.", + "pt-br": "Determina legendas de campo, notas de uso e legendas de tabela.", + }, + showDialogIcon: { + "en-us": "Show icon in the header", + "ru-ru": "Показывать значок в заголовке", + "es-es": "Mostrar icono en el encabezado", + "fr-fr": "Afficher l'icône dans l'en-tête", + "uk-ua": "Показати значок у заголовку", + "de-ch": "Symbol in der Kopfzeile anzeigen", + "pt-br": "Mostrar ícone no cabeçalho", + }, + scaleInterface: { + "en-us": "Scale Interface", + "ru-ru": "Интерфейс масштабирования", + "es-es": "Interfaz de escala", + "fr-fr": "Interface de balance", + "uk-ua": "Інтерфейс масштабу", + "de-ch": "Waagenschnittstelle", + "pt-br": "Interface de escala", + }, + scaleInterfaceDescription: { + "en-us": "Scale interface to match font size.", + "ru-ru": "Масштабируйте интерфейс в соответствии с размером шрифта.", + "es-es": "Escala la interfaz para que coincida con el tamaño de la fuente.", + "fr-fr": "Adapter l'interface à la taille de la police.", + "uk-ua": "Масштабуйте інтерфейс відповідно до розміру шрифту.", + "de-ch": + "Skalieren Sie die Benutzeroberfläche, um sie an die Schriftgröße anzupassen.", + "pt-br": "Dimensione a interface para corresponder ao tamanho da fonte.", + }, + displayAuthor: { + "en-us": "Show author in the tree", + "ru-ru": "Показать автора в дереве", + "es-es": "Mostrar autor en el árbol", + "fr-fr": "Afficher l'auteur dans l'arbre", + "uk-ua": "Показати автора в дереві", + "de-ch": "Autor im Baum anzeigen", + "pt-br": "Mostrar autor", + }, + welcomePage: { + "en-us": "Home Page", + "ru-ru": "Домашняя страница", + "es-es": "Página de inicio", + "fr-fr": "Page d'accueil", + "uk-ua": "Домашня сторінка", + "de-ch": "Startseite", + "pt-br": "Página inicial", + }, + content: { + "en-us": "Content", + "ru-ru": "Содержание", + "es-es": "Contenido", + "fr-fr": "Contenu", + "uk-ua": "Зміст", + "de-ch": "Inhalt", + "pt-br": "Contente", + }, + defaultImage: { + "en-us": "Specify Logo", + "ru-ru": "Укажите логотип", + "es-es": "Especificar logotipo", + "fr-fr": "Spécifier le logo", + "uk-ua": "Вкажіть логотип", + "de-ch": "Logo angeben", + "pt-br": "Especificar logotipo", + }, + customImage: { + "en-us": "Custom Image", + "ru-ru": "Пользовательское изображение", + "es-es": "Imagen personalizada", + "fr-fr": "Image personnalisée", + "uk-ua": "Спеціальне зображення", + "de-ch": "Benutzerdefiniertes Bild", + "pt-br": "Imagem personalizada", + }, + embeddedWebpage: { + "en-us": "Embedded web page", + "ru-ru": "Встроенная веб-страница", + "es-es": "Página web incrustada", + "fr-fr": "Page Web intégrée", + "uk-ua": "Вбудована веб-сторінка", + "de-ch": "Eingebettete Webseite", + "pt-br": "Página da web incorporada", + }, + embeddedWebpageDescription: { + "en-us": "A URL to a page that would be embedded on the home page:", + "ru-ru": "URL-адрес страницы, которая будет встроена в домашнюю страницу:", + "es-es": "Una URL a una página que se integrará en la página de inicio:", + "fr-fr": "Une URL vers une page qui serait intégrée à la page d'accueil :", + "uk-ua": "URL-адреса сторінки, яка буде вбудована на домашній сторінці:", + "de-ch": + "Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:", + "pt-br": "Um URL para uma página que seria incorporada na página inicial:", + }, + behavior: { + "en-us": "Behavior", + "ru-ru": "Поведение", + "es-es": "Comportamiento", + "fr-fr": "Comportement", + "uk-ua": "Поведінка", + "de-ch": "Verhalten", + "pt-br": "Comportamento", + }, + noRestrictionsMode: { + "en-us": "No restrictions mode", + "ru-ru": "Режим без ограничений", + "es-es": "Modo sin restricciones", + "fr-fr": "Mode sans restriction", + "uk-ua": "Режим без обмежень", + "de-ch": "Modus „Keine Einschränkungen“", + "pt-br": "Modo sem restrições", + }, + noRestrictionsModeWbDescription: { + "en-us": "Allows uploading data to any field in any table.", + "ru-ru": "Позволяет загружать данные в любое поле любой таблицы.", + "es-es": "Permite cargar datos a cualquier campo de cualquier tabla.", + "fr-fr": + "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", + "uk-ua": "Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.", + "de-ch": + "Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.", + "pt-br": "Permite carregar dados em qualquer campo de qualquer tabela.", + }, + noRestrictionsModeQueryDescription: { + "en-us": "Allows querying data from any field in any table.", + "ru-ru": "Позволяет запрашивать данные из любого поля любой таблицы.", + "es-es": "Permite consultar datos de cualquier campo de cualquier tabla.", + "fr-fr": + "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", + "uk-ua": "Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.", + "de-ch": + "Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.", + "pt-br": "Permite consultar dados de qualquer campo em qualquer tabela.", + }, + noRestrictionsModeWarning: { + "en-us": + "WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.", + "ru-ru": + "ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.", + "es-es": + "ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.", + "uk-ua": + "ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.", + "de-ch": + "WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.", + "fr-fr": + "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", + "pt-br": + "AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.", + }, + adminsOnlyPreference: { + "en-us": "You don't have permission to change this option", + "ru-ru": "У вас нет разрешения на изменение этой опции.", + "es-es": "No tienes permiso para cambiar esta opción", + "fr-fr": "Vous n'êtes pas autorisé à modifier cette option", + "uk-ua": "Ви не маєте дозволу змінювати цей параметр", + "de-ch": "Sie haben keine Berechtigung, diese Option zu ändern", + "pt-br": "Você não tem permissão para alterar esta opção", + }, + stickyScrolling: { + "en-us": "Sticky scroll bar", + "ru-ru": "Липкая полоса прокрутки", + "es-es": "Barra de desplazamiento fija", + "fr-fr": "Barre de défilement collante", + "uk-ua": "Липка смуга прокрутки", + "de-ch": "Klebrige Bildlaufleiste", + "pt-br": "Barra de rolagem fixa", + }, + foreground: { + "en-us": "Foreground", + "ru-ru": "Передний план", + "es-es": "Primer plano", + "fr-fr": "Premier plan", + "uk-ua": "Передній план", + "de-ch": "Vordergrund", + "pt-br": "Primeiro plano", + }, + background: { + "en-us": "Background", + "ru-ru": "Фон", + "es-es": "Fondo", + "fr-fr": "Arrière-plan", + "uk-ua": "Фон", + "de-ch": "Hintergrund", + "pt-br": "Fundo", + }, + sidebarTheme: { + "en-us": "Sidebar theme", + "de-ch": "Seitenleistenthema", + "es-es": "Tema de la barra lateral", + "fr-fr": "Thème de la barre latérale", + "ru-ru": "Тема боковой панели", + "uk-ua": "Тема бічної панелі", + "pt-br": "Tema da barra lateral", + }, + darkForeground: { + "en-us": "Foreground (dark theme)", + "ru-ru": "Передний план (тёмная тема)", + "es-es": "Primer plano (tema oscuro)", + "fr-fr": "Premier plan (thème sombre)", + "uk-ua": "Передній план (темна тема)", + "de-ch": "Vordergrund (dunkles Design)", + "pt-br": "Primeiro plano (tema escuro)", + }, + darkBackground: { + "en-us": "Background (dark theme)", + "ru-ru": "Фон (тёмная тема)", + "es-es": "Fondo (tema oscuro)", + "fr-fr": "Arrière-plan (thème sombre)", + "uk-ua": "Фон (темна тема)", + "de-ch": "Hintergrund (dunkles Design)", + "pt-br": "Plano de fundo (tema escuro)", + }, + accentColor1: { + "en-us": "Accent color 1", + "ru-ru": "Акцентный цвет 1", + "es-es": "Color de acento 1", + "fr-fr": "Couleur d'accent 1", + "uk-ua": "Акцентний колір 1", + "de-ch": "Akzentfarbe 1", + "pt-br": "Cor de destaque 1", + }, + accentColor2: { + "en-us": "Accent color 2", + "ru-ru": "Акцентный цвет 2", + "es-es": "Color de acento 2", + "fr-fr": "Couleur d'accent 2", + "uk-ua": "Акцентний колір 2", + "de-ch": "Akzentfarbe 2", + "pt-br": "Cor de destaque 2", + }, + accentColor3: { + "en-us": "Accent color 3", + "ru-ru": "Акцентный цвет 3", + "es-es": "Color de acento 3", + "fr-fr": "Couleur d'accent 3", + "uk-ua": "Акцентний колір 3", + "de-ch": "Akzentfarbe 3", + "pt-br": "Cor de destaque 3", + }, + accentColor4: { + "en-us": "Accent color 4", + "ru-ru": "Акцентный цвет 4", + "es-es": "Color de acento 4", + "fr-fr": "Couleur d'accent 4", + "uk-ua": "Акцентний колір 4", + "de-ch": "Akzentfarbe 4", + "pt-br": "Cor de destaque 4", + }, + accentColor5: { + "en-us": "Accent color 5", + "ru-ru": "Акцентный цвет 5", + "es-es": "Color de acento 5", + "fr-fr": "Couleur d'accent 5", + "uk-ua": "Акцентний колір 5", + "de-ch": "Akzentfarbe 5", + "pt-br": "Cor de destaque 5", + }, + spreadsheet: { + "en-us": "Spreadsheet", + "ru-ru": "Электронная таблица", + "es-es": "Hoja de cálculo", + "fr-fr": "Tableur", + "uk-ua": "Електронна таблиця", + "de-ch": "Kalkulationstabelle", + "pt-br": "Planilha", + }, + minSpareRows: { + "en-us": "Number of blank rows at the end", + "ru-ru": "Количество пустых строк в конце", + "es-es": "Número de filas en blanco al final", + "fr-fr": "Nombre de lignes vides à la fin", + "uk-ua": "Кількість порожніх рядків у кінці", + "de-ch": "Anzahl der leeren Zeilen am Ende", + "pt-br": "Número de linhas em branco no final", + }, + autoWrapCols: { + "en-us": "Navigate to the other side when reaching the edge column", + "ru-ru": "Достигнув крайней колонны, перейдите на другую сторону.", + "es-es": "Navegue hacia el otro lado al llegar a la columna del borde.", + "fr-fr": + "Naviguez de l’autre côté lorsque vous atteignez la colonne de bord", + "uk-ua": "Перейдіть на іншу сторону, коли досягнете краю колонки", + "de-ch": + "Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen", + "pt-br": "Navegue para o outro lado ao atingir a coluna da borda", + }, + autoWrapRows: { + "en-us": "Navigate to the other side when reaching the edge row", + "ru-ru": "Достигнув крайнего ряда, перейдите на другую сторону.", + "es-es": "Navegue hacia el otro lado al llegar a la fila del borde.", + "fr-fr": + "Naviguez de l’autre côté lorsque vous atteignez la rangée de bord", + "uk-ua": "Перейдіть на іншу сторону, коли досягнете крайнього ряду", + "de-ch": + "Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen", + "pt-br": "Navegue para o outro lado ao atingir a fileira de bordas", + }, + enterBeginsEditing: { + "en-us": "Enter key begins editing cell", + "ru-ru": "Клавиша Enter начинает редактирование ячейки.", + "es-es": "La tecla Enter inicia la edición de la celda", + "fr-fr": "La touche Entrée commence à modifier la cellule", + "uk-ua": "Клавіша Enter починає редагування клітинки", + "de-ch": "Mit der Eingabetaste beginnt die Bearbeitung der Zelle", + "pt-br": "A tecla Enter inicia a edição da célula", + }, + tabMoveDirection: { + "en-us": "Direction of movement when Tab key is pressed", + "ru-ru": "Направление движения при нажатии клавиши Tab", + "es-es": + "Dirección de movimiento cuando se presiona la tecla Tab", + "fr-fr": + "Sens de déplacement lorsque la touche Tabulation est enfoncée", + "uk-ua": "Напрямок руху при натисканні клавіші Tab", + "de-ch": "Bewegungsrichtung beim Drücken der Tab-Taste", + "pt-br": "Direção do movimento quando a tecla Tab é pressionada", + }, + tabMoveDirectionDescription: { + "en-us": + "You can move in the opposite direction by pressing Shift+Tab.", + "ru-ru": + "Вы можете двигаться в обратном направлении, нажав Shift+Tab.", + "es-es": + "Puedes moverte en la dirección opuesta presionando Shift+Tab.", + "fr-fr": + "Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.", + "uk-ua": + "Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.", + "de-ch": + "Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.", + "pt-br": + "Você pode mover na direção oposta pressionando Shift+Tab.", + }, + column: { + "en-us": "Column", + "ru-ru": "Столбец", + "es-es": "Columna", + "fr-fr": "Colonne", + "uk-ua": "Колонка", + "de-ch": "Spalte", + "pt-br": "Coluna", + }, + row: { + "en-us": "Row", + "ru-ru": "Ряд", + "es-es": "Fila", + "fr-fr": "Rangée", + "uk-ua": "рядок", + "de-ch": "Reihe", + "pt-br": "Linha", + }, + enterMoveDirection: { + "en-us": "Direction of movement when Enter key is pressed", + "ru-ru": "Направление движения при нажатии клавиши Enter", + "es-es": + "Dirección de movimiento cuando se presiona la tecla Enter", + "uk-ua": "Напрямок руху, коли натиснуто клавішу Enter", + "de-ch": "Bewegungsrichtung beim Drücken der Taste Enter", + "fr-fr": + "Direction du mouvement lorsque la touche Entrer est enfoncée", + "pt-br": + "Direção do movimento quando a tecla Enter é pressionada", + }, + enterMoveDirectionDescription: { + "en-us": + "You can move in the opposite direction by pressing Shift+Enter.", + "ru-ru": + "Вы можете двигаться в противоположном направлении, нажав Shift+Enter.", + "es-es": + "Puedes moverte en la dirección opuesta presionando Shift+Enter.", + "fr-fr": "Synonyme couleur.", + "uk-ua": + "Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.", + "de-ch": + "Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.", + "pt-br": + "Você pode mover na direção oposta pressionando Shift+Enter.", + }, + filterPickLists: { + "en-us": "Filter pick list items", + "ru-ru": "Фильтрация элементов списка выбора", + "es-es": "Filtrar elementos de la lista de selección", + "fr-fr": "Filtrer les éléments de la liste de sélection", + "uk-ua": "Фільтр вибору елементів списку", + "de-ch": "Auswahllistenelemente filtern", + "pt-br": "Filtrar itens da lista de seleção", + }, + exportFileDelimiter: { + "en-us": "Export file delimiter", + "ru-ru": "Разделитель файлов экспорта", + "es-es": "Delimitador de archivo de exportación", + "fr-fr": "Délimiteur de fichier d'exportation", + "uk-ua": "Роздільник файлу експорту", + "de-ch": "Dateitrennzeichen exportieren", + "pt-br": "Delimitador de arquivo de exportação", + }, + exportCsvUtf8Bom: { + "en-us": "Add UTF-8 BOM to CSV file exports", + "ru-ru": "Добавить UTF-8 BOM в экспорт CSV-файла", + "es-es": "Agregar BOM UTF-8 a las exportaciones de archivos CSV", + "fr-fr": "Ajouter UTF-8 BOM aux exportations de fichiers CSV", + "uk-ua": "Додайте специфікацію UTF-8 до експорту файлу CSVу", + "de-ch": "UTF-8 BOM zum CSV-Dateiexport hinzufügen", + "pt-br": "Adicionar UTF-8 BOM às exportações de arquivos CSV", + }, + exportCsvUtf8BomDescription: { + "en-us": + "Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.", + "ru-ru": "Корректное отображение экспортированных CSV-файлов в Excel.", + "es-es": + "Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.", + "fr-fr": + "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", + "uk-ua": "Змушує експорт файлів CSV правильно відображатися в Excel.", + "de-ch": + "Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.", + "pt-br": + "Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.", + }, + caseSensitive: { + "en-us": "Case-sensitive", + "ru-ru": "С учетом регистра", + "es-es": "Distingue mayúsculas y minúsculas", + "fr-fr": "Sensible aux majuscules et minuscules", + "uk-ua": "Чутливий до регістру", + "de-ch": "Groß- und Kleinschreibung beachten", + "pt-br": "Maiúsculas e minúsculas", + }, + caseInsensitive: { + "en-us": "Case-insensitive", + "ru-ru": "Без учета регистра", + "es-es": "Sin distinción entre mayúsculas y minúsculas", + "fr-fr": "Insensible à la casse", + "uk-ua": "Регістр не враховується", + "de-ch": "Groß-/Kleinschreibung wird nicht beachtet", + "pt-br": "Não diferencia maiúsculas de minúsculas", + }, + showNoReadTables: { + "en-us": 'Show tables without "Read" access', + "ru-ru": "Показывать таблицы без доступа «Чтение»", + "es-es": 'Mostrar tablas sin acceso de "Lectura"', + "fr-fr": 'Afficher les tableaux sans accès "Lecture"', + "uk-ua": "Показувати таблиці без доступу «Читання»", + "de-ch": "Tabellen ohne Lesezugriff anzeigen", + "pt-br": 'Mostrar tabelas sem acesso de "Leitura"', + }, + showNoAccessTables: { + "en-us": 'Show tables without "Create" access', + "ru-ru": "Показывать таблицы без права «Создать»", + "es-es": 'Mostrar tablas sin acceso "Crear"', + "fr-fr": 'Afficher les tableaux sans accès "Créer"', + "uk-ua": "Показувати таблиці без доступу «Створити»", + "de-ch": "Tabellen ohne „Erstellen“-Zugriff anzeigen", + "pt-br": 'Mostrar tabelas sem acesso "Criar"', + }, + textAreaAutoGrow: { + "en-us": "Text boxes grow automatically", + "ru-ru": "Текстовые поля увеличиваются автоматически", + "es-es": "Los cuadros de texto crecen automáticamente", + "fr-fr": "Les zones de texte s'agrandissent automatiquement", + "uk-ua": "Текстові поля збільшуються автоматично", + "de-ch": "Textfelder werden automatisch vergrößert", + "pt-br": "As caixas de texto crescem automaticamente", + }, + clearQueryFilters: { + "en-us": "Reset query filters", + "ru-ru": "Сбросить фильтры запроса", + "es-es": "Restablecer filtros de consulta", + "fr-fr": "Réinitialiser les filtres de requête", + "uk-ua": "Скинути фільтри запитів", + "de-ch": "Abfragefilter zurücksetzen", + "pt-br": "Redefinir filtros de consulta", + }, + clearQueryFiltersDescription: { + "en-us": "Clears all query filters when running a Report from a Form.", + "de-ch": + "Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.", + "es-es": + "Borra todos los filtros de consulta al ejecutar un informe desde un formulario.", + "fr-fr": + "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", + "ru-ru": "Очищает все фильтры запроса при запуске отчета из формы.", + "uk-ua": "Очищає всі фільтри запитів під час запуску звіту з форми.", + "pt-br": + "Limpa todos os filtros de consulta ao executar um relatório de um formulário.", + }, + queryParamtersFromForm: { + "en-us": "Show query filters when running a Report from a Form", + "de-ch": + "Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird", + "es-es": + "Mostrar filtros de consulta al ejecutar un informe desde un formulario", + "fr-fr": + "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", + "ru-ru": "Показывать фильтры запроса при запуске отчета из формы", + "uk-ua": "Показувати фільтри запитів під час запуску звіту з форми", + "pt-br": + "Mostrar filtros de consulta ao executar um relatório de um formulário", + }, + autoGrowAutoComplete: { + "en-us": "Allow autocomplete to grow as wide as need", + "ru-ru": + "Разрешить автозаполнению расширяться настолько, насколько это необходимо", + "es-es": "Permitir que el autocompletado crezca tanto como sea necesario", + "fr-fr": + "Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée", + "uk-ua": + "Дозволити автозаповнення розширюватися настільки, наскільки потрібно", + "de-ch": + "Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen", + "pt-br": + "Permitir que o preenchimento automático cresça o quanto for necessário", + }, + tableNameInTitle: { + "en-us": "Include table name in the browser page title", + "ru-ru": "Включить имя таблицы в заголовок страницы браузера", + "es-es": + "Incluir el nombre de la tabla en el título de la página del navegador", + "fr-fr": + "Inclure le nom de la table dans le titre de la page du navigateur", + "uk-ua": "Включіть назву таблиці в заголовок сторінки браузера", + "de-ch": "Tabellennamen in den Seitentitel des Browsers aufnehmen", + "pt-br": "Incluir nome da tabela no título da página do navegador", + }, + focusFirstField: { + "en-us": "Focus first field", + "de-ch": "Fokus erstes Feld", + "es-es": "Enfoque el primer campo", + "fr-fr": "Concentrez-vous sur le premier champ", + "ru-ru": "Фокус первого поля", + "uk-ua": "Перейти до першого поля", + "pt-br": "Foco primeiro no campo", + }, + doubleClickZoom: { + "en-us": "Double click to zoom", + "ru-ru": "Дважды щелкните, чтобы увеличить", + "es-es": "Haga doble clic para ampliar", + "fr-fr": "Double-cliquez pour zoomer", + "uk-ua": "Двічі клацніть, щоб збільшити", + "de-ch": "Zum Vergrößern doppelklicken", + "pt-br": "Clique duas vezes para ampliar", + }, + closePopupOnClick: { + "en-us": "Close pop-up on outside click", + "ru-ru": "Закрытие всплывающего окна при внешнем щелчке", + "es-es": "Cerrar ventana emergente al hacer clic desde fuera", + "fr-fr": "Fermer la pop-up lors d'un clic extérieur", + "uk-ua": "Закрити спливаюче вікно при зовнішньому клацанні", + "de-ch": "Popup bei externem Klick schließen", + "pt-br": "Fechar pop-up ao clicar fora", + }, + animateTransitions: { + "en-us": "Animate transitions", + "ru-ru": "Анимированные переходы", + "es-es": "Transiciones animadas", + "fr-fr": "Animer les transitions", + "uk-ua": "Анімація переходів", + "de-ch": "Übergänge animieren", + "pt-br": "Transições animadas", + }, + panInertia: { + "en-us": "Pan inertia", + "ru-ru": "Инерция пан", + "es-es": "Inercia de la sartén", + "fr-fr": "Inertie du bac", + "uk-ua": "Інерція панорами", + "de-ch": "Schwenkträgheit", + "pt-br": "Inércia da panela", + }, + mouseDrags: { + "en-us": "Mouse drags", + "ru-ru": "Перетаскивание мышью", + "es-es": "El ratón arrastra", + "uk-ua": "Виділіть відповідний підрядок", + "de-ch": "Maus zieht", + "fr-fr": "Mettre en surbrillance la sous-chaîne correspondante", + "pt-br": "Arrastos do mouse", + }, + scrollWheelZoom: { + "en-us": "Scroll wheel zoom", + "ru-ru": "Масштабирование с помощью колеса прокрутки", + "es-es": "Zoom con rueda de desplazamiento", + "fr-fr": "Zoom avec la molette de défilement", + "uk-ua": "Масштаб колеса прокрутки", + "de-ch": "Scrollrad-Zoom", + "pt-br": "Zoom da roda de rolagem", + }, + flexibleColumnWidth: { + "en-us": "Flexible column width", + "ru-ru": "Гибкая ширина столбца", + "es-es": "Ancho de columna flexible", + "fr-fr": "Largeur de colonne flexible", + "uk-ua": "Гнучка ширина колонки", + "de-ch": "Flexible Spaltenbreite", + "pt-br": "Largura de coluna flexível", + }, + flexibleSubGridColumnWidth: { + "en-us": "Flexible subview grid column width", + "ru-ru": "Гибкая ширина столбца сетки подпредставлений", + "es-es": "Ancho de columna de cuadrícula de subvista flexible", + "fr-fr": "Largeur de colonne de grille de sous-vue flexible", + "uk-ua": "Гнучка ширина стовпця сітки вкладеного перегляду", + "de-ch": "Flexible Spaltenbreite des Unteransichtsrasters", + "pt-br": "Largura flexível da coluna da grade de subvisualização", + }, + closeOnEsc: { + "en-us": "Close on ESC key press", + "ru-ru": "Закрыть нажатием клавиши ESC", + "es-es": "Cerrar al presionar la tecla ESC", + "fr-fr": "Icône et nom de la table", + "uk-ua": "Закриття натисканням клавіші ESC", + "de-ch": "Schließen durch Drücken der Taste ESC", + "pt-br": "Fechar ao pressionar a tecla ESC", + }, + closeOnOutsideClick: { + "en-us": "Close on outside click", + "ru-ru": "Закрытие по внешнему щелчку", + "es-es": "Cerrar al hacer clic desde fuera", + "fr-fr": "Fermer sur clic extérieur", + "uk-ua": "Закрийте зовнішнім клацанням", + "de-ch": "Schließen durch Klicken von außen", + "pt-br": "Fechar com clique externo", + }, + specifyNetworkBadge: { + "en-us": "Specify Network Badge", + "ru-ru": "Укажите сетевой значок", + "es-es": "Especificar la insignia de red", + "fr-fr": "Spécifier le badge réseau", + "uk-ua": "Укажіть значок мережі", + "de-ch": "Netzwerk-Badge angeben", + "pt-br": "Especificar emblema de rede", + }, + useAccessibleFullDatePicker: { + "en-us": "Use accessible full date picker", + "ru-ru": "Используйте доступный полный выбор даты", + "es-es": "Utilice el selector de fecha completo y accesible", + "fr-fr": "Utiliser un sélecteur de date complet accessible", + "uk-ua": "Використовуйте доступний повний засіб вибору дати", + "de-ch": "Verwenden Sie eine barrierefreie Datumsauswahl", + "pt-br": "Use o seletor de data completo acessível", + }, + useAccessibleMonthPicker: { + "en-us": "Use accessible month picker", + "ru-ru": "Используйте доступный выбор месяца", + "es-es": "Utilice el selector de meses accesible", + "fr-fr": "Utiliser le sélecteur de mois accessible", + "uk-ua": "Використовуйте доступний засіб вибору місяця", + "de-ch": "Verwenden Sie die barrierefreie Monatsauswahl", + "pt-br": "Use o seletor de meses acessível", + }, + rightAlignNumberFields: { + "en-us": "Right-Justify numeric fields", + "ru-ru": "Выравнивание числовых полей по правому краю", + "es-es": "Justificar a la derecha los campos numéricos", + "fr-fr": "Justifier à droite les champs numériques", + "uk-ua": "Вирівнювання по правому краю числових полів", + "de-ch": "Rechtsbündige Ausrichtung numerischer Felder", + "pt-br": "Justificar à direita campos numéricos", + }, + roundedCorners: { + "en-us": "Rounded corners", + "ru-ru": "Закругленные углы", + "es-es": "esquinas redondeadas", + "fr-fr": "Coins arrondis", + "uk-ua": "Заокруглені кути", + "de-ch": "Abgerundete Ecken", + "pt-br": "Cantos arredondados", + }, + showSubviewBorders: { + "en-us": "Show borders around subviews", + "de-ch": "Rahmen um Unteransichten anzeigen", + "es-es": "Mostrar bordes alrededor de las subvistas", + "fr-fr": "Afficher les bordures autour des sous-vues", + "pt-br": "Mostrar bordas ao redor das subvisualizações", + "ru-ru": "Показывать границы вокруг подпредставлений", + "uk-ua": "Показати межі навколо підвидів", + }, + limitMaxFieldWidth: { + "en-us": "Limit max field width", + "ru-ru": "Ограничить максимальную ширину поля", + "es-es": "Limitar el ancho máximo del campo", + "fr-fr": "Limiter la largeur maximale du champ", + "uk-ua": "Обмеження максимальної ширини поля", + "de-ch": "Maximale Feldbreite begrenzen", + "pt-br": "Limite máximo de largura do campo", + }, + condenseQueryResults: { + "en-us": "Condense query results", + "ru-ru": "Сжать результаты запроса", + "es-es": "Condensar los resultados de la consulta", + "fr-fr": "Condenser les résultats de la requête", + "uk-ua": "Згорнути результати запиту", + "de-ch": "Abfrageergebnisse verdichten", + "pt-br": "Condensar resultados da consulta", + }, + blurContentBehindDialog: { + "en-us": "Blur content behind the dialog", + "ru-ru": "Размытие содержимого за диалогом", + "es-es": "Desenfocar el contenido detrás del diálogo", + "fr-fr": "Flou le contenu derrière la boîte de dialogue", + "uk-ua": "Розмити вміст за діалоговим вікном", + "de-ch": "Inhalte hinter dem Dialog verwischen", + "pt-br": "Desfocar o conteúdo atrás do diálogo", + }, + collectionSortOrderDescription: { + "en-us": "This determines the visual order of collections.", + "ru-ru": "Это определяет визуальный порядок коллекций.", + "es-es": "Esto determina el orden visual de las colecciones.", + "fr-fr": "Ceci détermine l'ordre visuel des collections.", + "uk-ua": "Це визначає візуальний порядок колекцій.", + "de-ch": "Dies bestimmt die visuelle Reihenfolge der Sammlungen.", + "pt-br": "Isso determina a ordem visual das coleções.", + }, + recordSetRecordToOpen: { + "en-us": "Record to open by default", + "ru-ru": "Запись для открытия по умолчанию", + "es-es": "Registro para abrir por defecto", + "fr-fr": "Enregistrement à ouvrir par défaut", + "uk-ua": "Запис відкривається за умовчанням", + "de-ch": "Standardmäßig zu öffnender Datensatz", + "pt-br": "Gravar para abrir por padrão", + }, + altClickToSupressNewTab: { + "en-us": + "{altKeyName:string}+Click to suppress new tab", + "ru-ru": + "{altKeyName:string}+Нажмите , чтобы скрыть новую вкладку", + "es-es": + "{altKeyName:string}+Haga clic en para suprimir la nueva pestaña", + "fr-fr": + "{altKeyName:string}+Cliquez sur pour supprimer le nouvel onglet", + "uk-ua": + "{altKeyName:string}+Натисніть , щоб закрити нову вкладку", + "de-ch": + "{altKeyName:string}+Klicken Sie auf, um neue Registerkarten zu unterdrücken", + "pt-br": + "{altKeyName:string}+Clique em para suprimir a nova guia", + }, + altClickToSupressNewTabDescription: { + "en-us": + "{altKeyName:string}+Click a link that usually opens in a new tab to open it in the current tab.", + "ru-ru": + "{altKeyName:string}+Нажмите на ссылку, которая обычно открывается в новой вкладке, чтобы открыть ее в текущей вкладке.", + "es-es": + "{altKeyName:string}+Haga clic en un enlace que normalmente se abre en una nueva pestaña para abrirlo en la pestaña actual.", + "fr-fr": "Utiliser le sélecteur de mois accessible.", + "uk-ua": + "{altKeyName:string}+Натисніть посилання, яке зазвичай відкривається в новій вкладці, щоб відкрити його в поточній вкладці.", + "de-ch": + "{altKeyName:string}+Klicken Sie auf einen Link, der normalerweise in einem neuen Tab geöffnet wird, um ihn im aktuellen Tab zu öffnen.", + "pt-br": + "{altKeyName:string}+Clique em um link que geralmente abre em uma nova aba para abri-lo na aba atual.", + }, + makeFormDialogsModal: { + "en-us": "Make form dialogs gray out the background", + "ru-ru": "Сделать фон диалоговых окон серым", + "es-es": + "Hacer que los cuadros de diálogo del formulario tengan el fondo en gris", + "fr-fr": + "Rendre les boîtes de dialogue de formulaire grisées sur l'arrière-plan", + "uk-ua": "Зробіть діалогові вікна форми сірими фоном", + "de-ch": "Den Hintergrund von Formulardialogen ausgrauen", + "pt-br": + "Faça com que as caixas de diálogo do formulário fiquem com o fundo acinzentado", + }, + autoScrollTree: { + "en-us": "Auto scroll tree to focused node", + "ru-ru": "Автоматическая прокрутка дерева к выбранному узлу", + "es-es": "Desplazamiento automático del árbol al nodo enfocado", + "fr-fr": "Arbre de défilement automatique vers le nœud ciblé", + "uk-ua": "Автоматичне прокручування дерева до виділеного вузла", + "de-ch": "Automatisches Scrollen des Baums zum fokussierten Knoten", + "pt-br": "Rolagem automática da árvore para o nó em foco", + }, + sortByField: { + "en-us": "Order By Field", + "de-ch": "Nach Feld sortieren", + "es-es": "Ordenar por campo", + "fr-fr": "Trier par champ", + "pt-br": "Ordenar por campo", + "ru-ru": "Сортировать по полю", + "uk-ua": "Сортувати за полем", + }, + lineWrap: { + "en-us": "Line wrap", + "ru-ru": "Перенос строки", + "es-es": "Ajuste de línea", + "fr-fr": "Retour à la ligne", + "uk-ua": "Обтікання лініями", + "de-ch": "Zeilenumbruch", + "pt-br": "Quebra de linha", + }, + indentSize: { + "en-us": "Indent size", + "ru-ru": "Размер отступа", + "es-es": "Tamaño de sangría", + "fr-fr": "Taille du retrait", + "uk-ua": "Розмір відступу", + "de-ch": "Einzugsgröße", + "pt-br": "Tamanho do recuo", + }, + indentWithTab: { + "en-us": "Indent with Tab", + "ru-ru": "Отступ с помощью Tab", + "es-es": "Sangría con Tab", + "fr-fr": "Indenter avec Tabulation", + "uk-ua": "Відступ із Tab", + "de-ch": "Einrücken mit Tab", + "pt-br": "Recuo com Tab", + }, + formHeaderFormat: { + "en-us": "Form header format", + "ru-ru": "Формат заголовка формы", + "es-es": "Formato del encabezado del formulario", + "fr-fr": "Format d'en-tête de formulaire", + "uk-ua": "Формат заголовка форми", + "de-ch": "Formularkopfformat", + "pt-br": "Formato do cabeçalho do formulário", + }, + iconAndTableName: { + "en-us": "Icon and table name", + "ru-ru": "Значок и название таблицы", + "es-es": "Icono y nombre de la tabla", + "fr-fr": "Icône et nom de la table", + "uk-ua": "Значок і назва таблиці", + "de-ch": "Symbol und Tabellenname", + "pt-br": "Ícone e nome da tabela", + }, + tableIcon: { + "en-us": "Table icon", + "ru-ru": "Значок таблицы", + "es-es": "Icono de tabla", + "fr-fr": "Icône de tableau", + "uk-ua": "Значок таблиці", + "de-ch": "Tabellensymbol", + "pt-br": "Ícone de tabela", + }, + maxHeight: { + "en-us": "Max height", + "ru-ru": "Максимальная высота", + "es-es": "Altura máxima", + "fr-fr": "hauteur maximum", + "uk-ua": "Максимальна висота", + "de-ch": "Maximale Höhe", + "pt-br": "Altura máxima", + }, + autoComplete: { + "en-us": "Auto complete", + "ru-ru": "Автозаполнение", + "es-es": "Autocompletar", + "fr-fr": + "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux", + "uk-ua": + "Визначає підписи полів, примітки щодо використання та підписи таблиць", + "de-ch": "Autovervollständigung", + "pt-br": "Preenchimento automático", + }, + searchCaseSensitive: { + "en-us": "Case-sensitive search", + "es-es": "Búsqueda que distingue entre mayúsculas y minúsculas", + "fr-fr": "Recherche sensible à la casse", + "uk-ua": "Пошук з урахуванням регістру", + "de-ch": "Groß- und Kleinschreibung beachten", + "ru-ru": "Поиск с учетом регистра", + "pt-br": "Pesquisa com diferenciação entre maiúsculas e minúsculas", + }, + searchField: { + "en-us": "Search Field", + "ru-ru": "Поле поиска", + "es-es": "Campo de búsqueda", + "fr-fr": "Champ de recherche", + "uk-ua": "Поле пошуку", + "de-ch": "Suchfeld", + "pt-br": "Campo de pesquisa", + }, + createInteractions: { + "en-us": "Creating an interaction", + "ru-ru": "Создание взаимодействия", + "es-es": "Creando una interacción", + "fr-fr": "Créer une interaction", + "uk-ua": "Створення взаємодії", + "de-ch": "Erstellen einer Interaktion", + "pt-br": "Criando uma interação", + }, + useSpaceAsDelimiter: { + "en-us": "Use space as delimiter", + "ru-ru": "Используйте пробел в качестве разделителя", + "es-es": "Utilice el espacio como delimitador", + "fr-fr": "Utiliser l'espace comme délimiteur", + "uk-ua": "Використовуйте пробіл як роздільник", + "de-ch": "Leerzeichen als Trennzeichen verwenden", + "pt-br": "Use espaço como delimitador", + }, + useCommaAsDelimiter: { + "en-us": "Use comma as delimiter", + "ru-ru": "Используйте запятую в качестве разделителя", + "es-es": "Utilice la coma como delimitador", + "fr-fr": "Utiliser la virgule comme délimiteur", + "uk-ua": "Використовуйте кому як роздільник", + "de-ch": "Verwenden Sie Kommas als Trennzeichen.", + "pt-br": "Use vírgula como delimitador", + }, + useNewLineAsDelimiter: { + "en-us": "Use new line as delimiter", + "ru-ru": "Использовать новую строку в качестве разделителя", + "es-es": "Utilice nueva línea como delimitador", + "fr-fr": "Utiliser une nouvelle ligne comme délimiteur", + "uk-ua": "Використовуйте новий рядок як роздільник", + "de-ch": "Neue Zeile als Trennzeichen verwenden", + "pt-br": "Use nova linha como delimitador", + }, + useCustomDelimiters: { + "en-us": "Use custom delimiters", + "ru-ru": "Используйте пользовательские разделители", + "es-es": "Utilice delimitadores personalizados", + "fr-fr": "Utiliser des délimiteurs personnalisés", + "uk-ua": "Використовуйте спеціальні роздільники", + "de-ch": "Benutzerdefinierte Trennzeichen verwenden", + "pt-br": "Use delimitadores personalizados", + }, + useCustomDelimitersDescription: { + "en-us": + "A list of delimiters to use, in addition to the ones defined above. Put one delimiter per line.", + "ru-ru": + "Список разделителей, которые можно использовать в дополнение к указанным выше. Используйте по одному разделителю на строку.", + "es-es": + "Una lista de delimitadores para usar, además de los definidos anteriormente. Coloque un delimitador por línea.", + "fr-fr": + "Une liste de délimiteurs à utiliser, en plus de ceux définis ci-dessus. Mettez un délimiteur par ligne.", + "uk-ua": + "Список розділювачів для використання на додаток до визначених вище. Поставте один роздільник на рядок.", + "de-ch": + "Eine Liste der zu verwendenden Trennzeichen zusätzlich zu den oben definierten. Geben Sie pro Zeile ein Trennzeichen ein.", + "pt-br": + "Uma lista de delimitadores a serem usados, além dos definidos acima. Coloque um delimitador por linha.", + }, + detectAutomaticallyDescription: { + "en-us": "Detect automatically based on catalog number format.", + "ru-ru": "Автоматическое определение на основе формата каталожного номера.", + "es-es": + "Detectar automáticamente según el formato del número de catálogo.", + "fr-fr": + "Détecter automatiquement en fonction du format du numéro de catalogue.", + "uk-ua": "Визначати автоматично на основі формату номера каталогу.", + "de-ch": "Automatische Erkennung basierend auf dem Katalognummernformat.", + "pt-br": + "Detecte automaticamente com base no formato do número de catálogo.", + }, + use: { + comment: "Verb", + "en-us": "Use", + "ru-ru": "Использовать", + "es-es": "Usar", + "fr-fr": "Utiliser", + "uk-ua": "використання", + "de-ch": "Verwenden", + "pt-br": "Usar", + }, + dontUse: { + "en-us": "Don’t use", + "ru-ru": "Не использовать", + "es-es": "No utilizar", + "fr-fr": "Zoom avec la molette de défilement", + "uk-ua": "Масштаб колеса прокрутки", + "de-ch": "Nicht verwenden", + "pt-br": "Não use", + }, + position: { + "en-us": "Position", + "es-es": "Posición", + "fr-fr": "Position", + "ru-ru": "Позиция", + "uk-ua": "Позиція", + "de-ch": "Position", + "pt-br": "Posição", + }, + top: { + "en-us": "Top", + "es-es": "Arriba", + "fr-fr": "Haut", + "ru-ru": "Вершина", + "uk-ua": "Топ", + "de-ch": "Spitze", + "pt-br": "Principal", + }, + bottom: { + "en-us": "Bottom", + "es-es": "Abajo", + "ru-ru": "Нижний", + "uk-ua": "Дно", + "de-ch": "Unten", + "fr-fr": "Bas", + "pt-br": "Fundo", + }, + left: { + "en-us": "Left", + "es-es": "Izquierda", + "fr-fr": "Gauche", + "ru-ru": "Левый", + "uk-ua": "Ліворуч", + "de-ch": "Links", + "pt-br": "Esquerda", + }, + right: { + "en-us": "Right", + "es-es": "Bien", + "fr-fr": "Droite", + "ru-ru": "Верно", + "uk-ua": "правильно", + "de-ch": "Rechts", + "pt-br": "Certo", + }, + showUnsavedIndicator: { + "en-us": "Show unsaved changes indicator", + "ru-ru": "Показать индикатор несохраненных изменений", + "es-es": "Mostrar indicador de cambios no guardados", + "fr-fr": "Afficher l'indicateur de modifications non enregistrées", + "uk-ua": "Показати індикатор незбережених змін", + "de-ch": "Indikator für nicht gespeicherte Änderungen anzeigen", + "pt-br": "Mostrar indicador de alterações não salvas", + }, + showUnsavedIndicatorDescription: { + "en-us": + 'Show an "*" in the tab title when there are unsaved changes in the current tab.', + "es-es": + 'Mostrar un "*" en el título de la pestaña cuando haya cambios sin guardar en la pestaña actual.', + "fr-fr": + "Afficher un \"*\" dans le titre de l'onglet lorsqu'il y a des modifications non enregistrées dans l'onglet actuel.", + "ru-ru": + "Отображать «*» в заголовке вкладки, если на текущей вкладке есть несохраненные изменения.", + "uk-ua": + "Показувати «*» у заголовку вкладки, якщо в поточній вкладці є незбережені зміни.", + "de-ch": + "Zeigen Sie im Registerkartentitel ein „*“ an, wenn in der aktuellen Registerkarte nicht gespeicherte Änderungen vorhanden sind.", + "pt-br": + 'Exibir um "*" no título da aba quando houver alterações não salvas na aba atual.', + }, + autoPopulateDescription: { + "en-us": + "Auto populate the merged record with values from duplicates when opening the merging dialog.", + "ru-ru": + "Автоматически заполнять объединенную запись значениями из дубликатов при открытии диалогового окна слияния.", + "de-ch": + "Füllen Sie den zusammengeführten Datensatz beim Öffnen des Zusammenführungsdialogs automatisch mit Werten aus Duplikaten.", + "es-es": + "Rellene automáticamente el registro fusionado con valores de duplicados al abrir el cuadro de diálogo de fusión.", + "fr-fr": + "Remplir automatiquement l'enregistrement fusionné avec les valeurs des doublons lors de l'ouverture de la boîte de dialogue de fusion.", + "uk-ua": + "Автоматичне заповнення об’єднаного запису значеннями з дублікатів під час відкриття діалогового вікна об’єднання.", + "pt-br": + "Preencha automaticamente o registro mesclado com valores de duplicatas ao abrir a caixa de diálogo de mesclagem.", + }, + autoCreateVariants: { + "en-us": "Automatically create {agentVariantTable:string} records", + "ru-ru": "Автоматически создавать записи {agentVariantTable:string}", + "de-ch": "{agentVariantTable:string}-Datensätze automatisch erstellen", + "es-es": "Crear automáticamente registros {agentVariantTable:string}", + "fr-fr": + "Créer automatiquement des enregistrements {agentVariantTable:string}", + "uk-ua": "Автоматично створювати записи {agentVariantTable:string}", + "pt-br": "Criar automaticamente registros {agentVariantTable:string}", + }, + autoCreateVariantsDescription: { + "en-us": + "When merging agents, automatically create {agentVariantTable:string} records based on the variations of first name/last name.", + "ru-ru": + "При объединении агентов автоматически создавать записи {agentVariantTable:string} на основе вариаций имени/фамилии.", + "de-ch": + "Beim Zusammenführen von Agenten werden automatisch {agentVariantTable:string}-Datensätze basierend auf den Variationen von Vorname/Nachname erstellt.", + "es-es": + "Al fusionar agentes, cree automáticamente registros {agentVariantTable:string} basados en las variaciones de nombre/apellido.", + "fr-fr": + "Lors de la fusion d'agents, créez automatiquement des enregistrements {agentVariantTable:string} en fonction des variations du prénom/nom.", + "uk-ua": + "Під час об’єднання агентів автоматично створювати записи {agentVariantTable:string} на основі варіацій імені/прізвища.", + "pt-br": + "Ao mesclar agentes, crie automaticamente registros {agentVariantTable:string} com base nas variações de nome/sobrenome.", + }, + collectionPreferences: { + "en-us": "Collection Preferences", + "de-ch": "Sammlungseinstellungen", + "es-es": "Preferencias de colección", + "fr-fr": "Personnalisation", + "ru-ru": "Настройки коллекции", + "uk-ua": "Налаштування", + "pt-br": "Preferências de coleção", + }, + rememberDialogSizes: { + "en-us": "Remember dialog window sizes", + "ru-ru": "Запомните размеры диалоговых окон", + "es-es": "Recordar los tamaños de las ventanas de diálogo", + "fr-fr": "Mémoriser les tailles des fenêtres de dialogue", + "uk-ua": "Запам'ятайте розміри діалогових вікон", + "de-ch": "Dialogfenstergrößen merken", + "pt-br": "Lembrar tamanhos de janelas de diálogo", + }, + rememberDialogPositions: { + "en-us": "Remember dialog window positions", + "ru-ru": "Запомнить позиции диалоговых окон", + "es-es": "Recordar las posiciones de las ventanas de diálogo", + "fr-fr": "Mémoriser les positions des fenêtres de dialogue", + "uk-ua": "Запам'ятовуйте положення діалогового вікна", + "de-ch": "Dialogfensterpositionen merken", + "pt-br": "Lembrar posições da janela de diálogo", + }, + autoPlayMedia: { + "en-us": "Automatically play media", + "ru-ru": "Автоматически воспроизводить медиа", + "es-es": "Reproducir automáticamente medios", + "fr-fr": "Lire automatiquement les médias", + "uk-ua": "Автоматичне відтворення медіа", + "de-ch": "Medien automatisch abspielen", + "pt-br": "Reproduzir mídia automaticamente", + }, + useCustomTooltips: { + "en-us": "Use modern tooltips", + "ru-ru": "Используйте современные подсказки", + "es-es": "Utilice información sobre herramientas moderna", + "fr-fr": "Utiliser des info-bulles modernes", + "uk-ua": "Використовуйте сучасні підказки", + "de-ch": "Verwenden Sie moderne Tooltips", + "pt-br": "Use dicas de ferramentas modernas", + }, + alwaysUseQueryBuilder: { + "en-us": "Always use query builder search inside of search form", + "de-ch": + "Verwenden Sie innerhalb des Suchformulars immer die Abfragegeneratorsuche", + "es-es": + "Utilice siempre la búsqueda del generador de consultas dentro del formulario de búsqueda", + "fr-fr": + "Utilisez toujours la recherche du générateur de requêtes dans le formulaire de recherche", + "ru-ru": "Всегда используйте конструктор запросов внутри формы поиска.", + "uk-ua": "Завжди використовуйте пошук конструктора запитів у формі пошуку", + "pt-br": + "Sempre use a pesquisa do construtor de consultas dentro do formulário de pesquisa", + }, + localizeResourceNames: { + "en-us": "Localize the names of recognized app resources", + "de-ch": "Lokalisieren Sie die Namen erkannter App-Ressourcen", + "es-es": + "Localizar los nombres de los recursos de aplicaciones reconocidos", + "fr-fr": "Localiser les noms des ressources d'application reconnues", + "ru-ru": "Локализуйте названия распознанных ресурсов приложения", + "uk-ua": "Локалізувати назви розпізнаних ресурсів програми", + "pt-br": "Localize os nomes dos recursos de aplicativos reconhecidos", + }, + splitLongXml: { + "en-us": "Split long lines of XML into multiple lines", + "de-ch": "Teilen Sie lange XML-Zeilen in mehrere Zeilen auf", + "es-es": "Dividir líneas largas de XML en varias líneas", + "fr-fr": "Diviser les longues lignes de XML en plusieurs lignes", + "ru-ru": "Разделить длинные строки XML на несколько строк", + "uk-ua": "Розділіть довгі рядки XML на кілька рядків", + "pt-br": "Dividir longas linhas de XML em várias linhas", + }, + url: { + "en-us": "URL", + "de-ch": "URL", + "es-es": "URL", + "fr-fr": "URL", + "uk-ua": "URL", + "ru-ru": "URL", + "pt-br": "URL", + }, + pickAttachment: { + "en-us": "Pick an attachment", + "es-es": "Elige un archivo adjunto", + "fr-fr": "Choisissez une pièce jointe", + "ru-ru": "Выберите вложение", + "uk-ua": "Виберіть вкладення", + "de-ch": "Wählen Sie einen Anhang", + "pt-br": "Escolha um anexo", + }, + attachmentFailed: { + "en-us": "The attachment failed to load.", + "de-ch": "Der Anhang konnte nicht geladen werden.", + "es-es": "No se pudo cargar el archivo adjunto.", + "fr-fr": "La pièce jointe n'a pas pu être chargée.", + "ru-ru": "Не удалось загрузить вложение.", + "uk-ua": "Не вдалося завантажити вкладений файл.", + "pt-br": "O anexo não pôde ser carregado.", + }, + pickImage: { + "en-us": "Pick an image", + "de-ch": "Wählen Sie ein Bild aus", + "es-es": "Elige una imagen", + "fr-fr": "Choisissez une image", + "ru-ru": "Выберите изображение", + "uk-ua": "Виберіть зображення", + "pt-br": "Escolha uma imagem", + }, + customLogo: { + "en-us": "Expanded Image URL", + "de-ch": "Erweiterte Bild-URL", + "es-es": "URL de imagen expandida", + "fr-fr": "URL de l'image étendue", + "ru-ru": "URL-адрес развернутого изображения", + "uk-ua": "Розширена URL-адреса зображення", + "pt-br": "URL da imagem expandida", + }, + customLogoCollapsed: { + "en-us": "Collapsed Image URL", + "de-ch": "URL des minimierten Bildes", + "es-es": "URL de imagen contraída", + "fr-fr": "URL de l'image réduite", + "ru-ru": "URL-адрес свернутого изображения", + "uk-ua": "URL-адреса згорнутого зображення", + "pt-br": "URL da imagem recolhida", + }, + customLogoDescription: { + "en-us": + "A URL to an image that would be displayed next to the Specify logo in the navigation menu.", + "de-ch": + "Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.", + "es-es": + "Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.", + "fr-fr": + "Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.", + "ru-ru": + "URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.", + "uk-ua": + "URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.", + "pt-br": + "Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.", + }, + showLineNumber: { + "en-us": "Show query result line number", + "de-ch": "Zeilennummer des Abfrageergebnisses anzeigen", + "es-es": "Mostrar el número de línea del resultado de la consulta", + "fr-fr": "Afficher le numéro de ligne du résultat de la requête", + "ru-ru": "Показать номер строки результата запроса", + "uk-ua": "Показати номер рядка результату запиту", + "pt-br": "Mostrar número da linha do resultado da consulta", + }, + saveButtonColor: { + "en-us": "Save button color", + "de-ch": "Farbe der Schaltfläche „Speichern“", + "es-es": "Guardar el color del botón", + "fr-fr": "Couleur du bouton Enregistrer", + "ru-ru": "Сохранить цвет кнопки", + "uk-ua": "Зберегти колір кнопки", + "pt-br": "Cor do botão Salvar", + }, + secondaryButtonColor: { + "en-us": "Secondary button color", + "es-es": "Color del botón secundario", + "fr-fr": "Couleur du bouton secondaire", + "ru-ru": "Цвет вторичной кнопки", + "uk-ua": "Колір вторинної кнопки", + "de-ch": "Sekundäre Schaltflächenfarbe", + "pt-br": "Cor do botão secundário", + }, + secondaryLightButtonColor: { + "en-us": "Secondary light button color", + "de-ch": "Farbe der sekundären Lichttaste", + "es-es": "Color del botón de luz secundaria", + "fr-fr": "Couleur du bouton lumineux secondaire", + "ru-ru": "Цвет кнопки дополнительного освещения", + "uk-ua": "Колір вторинної світлової кнопки", + "pt-br": "Cor do botão de luz secundária", + }, + dangerButtonColor: { + "en-us": "Danger button color", + "de-ch": "Farbe der Gefahrenschaltfläche", + "es-es": "Color del botón de peligro", + "fr-fr": "Couleur du bouton de danger", + "ru-ru": "Цвет кнопки «Опасность»", + "uk-ua": "Колір кнопки небезпеки", + "pt-br": "Cor do botão de perigo", + }, + infoButtonColor: { + "en-us": "Info button color", + "de-ch": "Farbe der Info-Schaltfläche", + "es-es": "Color del botón de información", + "fr-fr": "Couleur du bouton d'information", + "ru-ru": "Цвет кнопки информации", + "uk-ua": "Колір інформаційної кнопки", + "pt-br": "Cor do botão de informações", + }, + warningButtonColor: { + "en-us": "Warning button color", + "de-ch": "Farbe der Warnschaltfläche", + "es-es": "Color del botón de advertencia", + "fr-fr": "Couleur du bouton d'avertissement", + "ru-ru": "Цвет кнопки предупреждения", + "uk-ua": "Колір кнопки попередження", + "pt-br": "Cor do botão de aviso", + }, + successButtonColor: { + "en-us": "Success button color", + "de-ch": "Farbe der Schaltfläche „Erfolg“", + "es-es": "Color del botón de éxito", + "fr-fr": "Couleur du bouton de réussite", + "ru-ru": "Цвет кнопки «Успех»", + "uk-ua": "Колір кнопки успіху", + "pt-br": "Cor do botão de sucesso", + }, + openAsReadOnly: { + "en-us": "Open all records in read-only mode", + "de-ch": "Alle Datensätze im schreibgeschützten Modus öffnen", + "es-es": "Abrir todos los registros en modo de solo lectura", + "fr-fr": "Ouvrir tous les enregistrements en mode lecture seule", + "ru-ru": "Открыть все записи в режиме только для чтения", + "uk-ua": "Відкрити всі записи в режимі лише для читання", + "pt-br": "Abra todos os registros no modo somente leitura", + }, + displayBasicView: { + "en-us": "Display basic view", + "de-ch": "Basisansicht anzeigen", + "es-es": "Mostrar vista básica", + "fr-fr": "Afficher la vue de base", + "ru-ru": "Отобразить базовый вид", + "uk-ua": "Відобразити базовий вигляд", + "pt-br": "Exibir visualização básica", + }, + showComparisonOperatorsForString: { + "en-us": "Show comparison operators for text-based fields", + "de-ch": "Vergleichsoperatoren für textbasierte Felder anzeigen", + "es-es": "Mostrar operadores de comparación para campos basados en texto", + "fr-fr": "Afficher les opérateurs de comparaison pour les champs textuels", + "pt-br": "Mostrar operadores de comparação para campos baseados em texto", + "ru-ru": "Показать операторы сравнения для текстовых полей", + "uk-ua": "Показати оператори порівняння для текстових полів", + }, + showComparisonOperatorsDescription: { + "en-us": + "Allows the following filters to apply to text fields: Greater Than, Less Than, Greater Than or Equal to, and Less Than or Equal to", + "de-ch": + "Ermöglicht die Anwendung der folgenden Filter auf Textfelder: Größer als, Kleiner als, Größer als oder gleich und Kleiner als oder gleich", + "es-es": + "Permite aplicar los siguientes filtros a los campos de texto: Mayor que, Menor que, Mayor o igual que y Menor o igual que", + "fr-fr": + "Permet d'appliquer les filtres suivants aux champs de texte : Supérieur à, Inférieur à, Supérieur ou égal à et Inférieur ou égal à", + "pt-br": + "Permite que os seguintes filtros sejam aplicados aos campos de texto: Maior que, Menor que, Maior ou igual a e Menor ou igual a", + "ru-ru": + "Позволяет применять к текстовым полям следующие фильтры: «Больше», «Меньше», «Больше или равно» и «Меньше или равно».", + "uk-ua": + "Дозволяє застосовувати до текстових полів такі фільтри: «Більше ніж», «Менше ніж», «Більше або дорівнює» та «Менше або дорівнює»", + }, + basicView: { + "en-us": "Basic view", + "de-ch": "Basisansicht", + "es-es": "Vista básica", + "fr-fr": "Vue de base", + "ru-ru": "Базовый вид", + "uk-ua": "Основний вигляд", + "pt-br": "Visão básica", + }, + detailedView: { + "en-us": "Detailed view", + "de-ch": "Detailansicht", + "es-es": "Vista detallada", + "fr-fr": "Vue détaillée", + "ru-ru": "Подробный вид", + "uk-ua": "Детальний вигляд", + "pt-br": "Visão detalhada", + }, + attachmentPreviewMode: { + "en-us": "Attachment preview mode", + "de-ch": "Anhangsvorschaumodus", + "es-es": "Modo de vista previa de archivos adjuntos", + "fr-fr": "Mode d'aperçu des pièces jointes", + "ru-ru": "Режим предварительного просмотра вложений", + "uk-ua": "Режим попереднього перегляду вкладених файлів", + "pt-br": "Modo de visualização de anexos", + }, + fullResolution: { + "en-us": "Full Resolution", + "de-ch": "Volle Auflösung", + "es-es": "Resolución completa", + "fr-fr": "Pleine résolution", + "ru-ru": "Полное разрешение", + "uk-ua": "Повна роздільна здатність", + "pt-br": "Resolução completa", + }, + thumbnail: { + "en-us": "Thumbnail", + "de-ch": "Miniaturansicht", + "es-es": "Uña del pulgar", + "fr-fr": "Vignette", + "ru-ru": "Миниатюра", + "uk-ua": "Мініатюра", + "pt-br": "Miniatura", + }, + addSearchBarHomePage: { + "en-us": "Add Search Bar on home page", + "de-ch": "Suchleiste auf der Startseite hinzufügen", + "es-es": "Agregar barra de búsqueda en la página de inicio", + "fr-fr": "Ajouter une barre de recherche sur la page d'accueil", + "ru-ru": "Добавить панель поиска на домашнюю страницу", + "uk-ua": "Додайте рядок пошуку на головну сторінку", + "pt-br": "Adicionar barra de pesquisa na página inicial", + }, + inheritanceCatNumberPref: { + "en-us": + "Enable the inheritance of the primary catalog number to its empty siblings.", + "de-ch": + "Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.", + "es-es": + "Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.", + "fr-fr": + "Activez l'héritage du numéro de catalogue principal vers ses frères vides.", + "pt-br": + "Habilitar a herança do número do catálogo primário para seus irmãos vazios.", + "ru-ru": + "Включить наследование основного каталожного номера его пустыми родственными номерами.", + "uk-ua": + "Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.", + }, + inheritanceCatNumberParentCOPref: { + "en-us": + "Enable the inheritance of the parent catalog number to its empty children.", + "de-ch": + "Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.", + "es-es": + "Habilitar la herencia del número de catálogo padre a sus hijos vacíos.", + "fr-fr": + "Activer l'héritage du numéro de catalogue parent à ses enfants vides.", + "pt-br": + "Habilitar a herança do número do catálogo pai para seus filhos vazios.", + "ru-ru": + "Включить наследование родительского каталожного номера его пустыми дочерними элементами.", + "uk-ua": + "Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.", + }, +} as const; + + +export const preferencesGeneralText = createDictionary( + preferencesGeneralDictionary +); diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index 7ea7903bc8c..bed614cba81 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -1,726 +1,17 @@ /** - * Localization strings for the preferences menu (split into manageable sections). + * Localization strings for the preferences menu (aggregated from modular sections). * * @module */ import { createDictionary } from './utils'; -import { - preferencesContentText, -} from './preferences.content'; -import { - preferencesBehaviorText, -} from './preferences.behavior'; +import { preferencesBehaviorDictionary } from './preferences.behavior'; +import { preferencesContentDictionary } from './preferences.content'; +import { preferencesGeneralDictionary } from './preferences.general'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file -export const preferencesGeneralStrings = { - preferences: { - 'en-us': 'Preferences', - 'ru-ru': 'Настройки', - 'es-es': 'Preferencias', - 'fr-fr': 'Préférences', - 'uk-ua': 'Уподобання', - 'de-ch': 'Einstellungen', - 'pt-br': 'Preferências', - }, - customization: { - 'en-us': 'Customization', - 'ru-ru': 'Настройка', - 'es-es': 'Personalización', - 'fr-fr': 'Personnalisation', - 'uk-ua': 'Спеціальнізація', - 'de-ch': 'Anpassung', - 'pt-br': 'Personalização', - }, - userPreferences: { - 'en-us': 'User Preferences', - 'ru-ru': 'Настройки пользователя', - 'es-es': 'Preferencias del usuario', - 'fr-fr': "Préférences de l'utilisateur", - 'uk-ua': 'Налаштування користувача', - 'de-ch': 'Benutzereinstellungen', - 'pt-br': 'Preferências do usuário', - }, - defaultUserPreferences: { - 'en-us': 'Default User Preferences', - 'ru-ru': 'Настройки пользователя по умолчанию', - 'es-es': 'Preferencias de usuario predeterminadas', - 'fr-fr': 'Préférences utilisateur par défaut', - 'uk-ua': 'Параметри користувача за умовчанням', - 'de-ch': 'Standardbenutzereinstellungen', - 'pt-br': 'Preferências de usuário padrão', - }, - general: { - 'en-us': 'General', - 'ru-ru': 'Общий', - 'es-es': 'General', - 'fr-fr': 'Image personnalisée', - 'uk-ua': 'Спеціальне зображення', - 'de-ch': 'Allgemein', - 'pt-br': 'Em geral', - }, - ui: { - 'en-us': 'User Interface', - 'ru-ru': 'Пользовательский интерфейс', - 'es-es': 'Interfaz de usuario', - 'fr-fr': 'Interface utilisateur', - 'uk-ua': 'Інтерфейс користувача', - 'de-ch': 'Benutzeroberfläche', - 'pt-br': 'Interface do usuário', - }, - theme: { - 'en-us': 'Theme', - 'ru-ru': 'Тема', - 'es-es': 'Tema', - 'fr-fr': 'Thème', - 'uk-ua': 'Тема', - 'de-ch': 'Thema', - 'pt-br': 'Tema', - }, - useSystemSetting: { - 'en-us': 'Use system setting', - 'ru-ru': 'Использовать системные настройки', - 'es-es': 'Utilizar la configuración del sistema', - 'fr-fr': 'Utiliser les paramètres du système', - 'uk-ua': 'Використовуйте налаштування системи', - 'de-ch': 'Systemeinstellung verwenden', - 'pt-br': 'Usar configuração do sistema', - }, - inheritOsSettings: { - 'en-us': 'Copies value from your Operating System settings', - 'ru-ru': 'Копирует значение из настроек вашей операционной системы', - 'es-es': 'Copia el valor de la configuración de su sistema operativo', - 'fr-fr': "Copie la valeur des paramètres de votre système d'exploitation", - 'uk-ua': 'Копіює значення з налаштувань вашої операційної системи', - 'de-ch': 'Übernimmt den Wert aus Ihren Betriebssystemeinstellungen', - 'pt-br': 'Copia o valor das configurações do seu sistema operacional', - }, - light: { - comment: 'Light mode', - 'en-us': 'Light', - 'ru-ru': 'Свет', - 'es-es': 'Claro', - 'fr-fr': 'Lumière', - 'uk-ua': 'світло', - 'de-ch': 'Hell', - 'pt-br': 'Luz', - }, - dark: { - comment: 'Dark mode', - 'en-us': 'Dark', - 'ru-ru': 'Темный', - 'es-es': 'Oscuro', - 'fr-fr': 'Sombre', - 'uk-ua': 'Темний', - 'de-ch': 'Dunkel', - 'pt-br': 'Escuro', - }, - reduceMotion: { - 'en-us': 'Reduce motion', - 'ru-ru': 'Уменьшите движение', - 'es-es': 'Reducir el movimiento', - 'fr-fr': 'Réduire les mouvements', - 'uk-ua': 'Зменшити рух', - 'de-ch': 'Bewegung reduzieren', - 'pt-br': 'Reduzir movimento', - }, - reduceMotionDescription: { - 'en-us': 'Disable non-essential animations and transitions.', - 'ru-ru': 'Отключите ненужные анимации и переходы.', - 'es-es': 'Desactivar animaciones y transiciones no esenciales.', - 'fr-fr': 'Désactivez les animations et les transitions non essentielles.', - 'uk-ua': "Вимкніть необов'язкову анімацію та переходи.", - 'de-ch': 'Nicht erforderliche Animationen und Übergänge deaktivieren.', - 'pt-br': 'Desabilite animações e transições não essenciais.', - }, - reduceTransparency: { - 'en-us': 'Reduce transparency', - 'ru-ru': 'Уменьшить прозрачность', - 'es-es': 'Reducir la transparencia', - 'fr-fr': 'Réduire la transparence', - 'uk-ua': 'Зменшити прозорість', - 'de-ch': 'Transparenz reduzieren', - 'pt-br': 'Reduzir a transparência', - }, - reduceTransparencyDescription: { - 'en-us': - 'Whether to disable translucent backgrounds for user interface components whenever possible (e.g. table headers in tree view).', - 'ru-ru': - 'Следует ли отключать полупрозрачный фон для компонентов пользовательского интерфейса, когда это возможно (например, заголовки таблиц в древовидной структуре).', - 'es-es': - 'Si se deben deshabilitar los fondos translúcidos para los componentes de la interfaz de usuario siempre que sea posible (por ejemplo, encabezados de tabla en la vista de árbol).', - 'fr-fr': - "S'il faut désactiver les arrière-plans translucides pour les composants de l'interface utilisateur chaque fois que possible (par exemple, les en-têtes de tableau dans l'arborescence).", - 'uk-ua': - 'Чи вимикати напівпрозорий фон для компонентів інтерфейсу користувача, коли це можливо (наприклад, заголовки таблиць у перегляді дерева).', - 'de-ch': - 'Durchsichtige Hintergründe für Benutzeroberflächenkomponenten wann immer möglich deaktivieren (z. B. Tabellenüberschriften in der Baumansicht).', - 'pt-br': - 'Se deve desabilitar fundos translúcidos para componentes da interface do usuário sempre que possível (por exemplo, cabeçalhos de tabela na visualização em árvore).', - }, - contrast: { - 'en-us': 'Contrast', - 'ru-ru': 'Контраст', - 'es-es': 'Contraste', - 'fr-fr': 'Contraste', - 'uk-ua': 'Контраст', - 'de-ch': 'Kontrast', - 'pt-br': 'Contraste', - }, - increase: { - 'en-us': 'Increase', - 'ru-ru': 'Увеличивать', - 'es-es': 'Aumentar', - 'fr-fr': 'Augmenter', - 'uk-ua': 'Збільшити', - 'de-ch': 'Erhöhen', - 'pt-br': 'Aumentar', - }, - reduce: { - 'en-us': 'Reduce', - 'ru-ru': 'Уменьшать', - 'es-es': 'Reducir', - 'fr-fr': 'Réduire', - 'uk-ua': 'Зменшити', - 'de-ch': 'Verringern', - 'pt-br': 'Reduzir', - }, - noPreference: { - 'en-us': 'No preference', - 'ru-ru': 'Нет предпочтений', - 'es-es': 'Sin preferencia', - 'fr-fr': 'Pas de préférence', - 'uk-ua': 'Без переваг', - 'de-ch': 'Keine Präferenz', - 'pt-br': 'Sem preferência', - }, - fontSize: { - 'en-us': 'Font size', - 'ru-ru': 'Размер шрифта', - 'es-es': 'Tamaño de fuente', - 'fr-fr': 'Taille de police', - 'uk-ua': 'Розмір шрифту', - 'de-ch': 'Schriftgrösse', - 'pt-br': 'Tamanho da fonte', - }, - fontFamily: { - 'en-us': 'Font family', - 'ru-ru': 'Семейство шрифтов', - 'es-es': 'Familia de fuentes', - 'fr-fr': 'Famille de polices', - 'uk-ua': 'Сімейство шрифтів', - 'de-ch': 'Schrift-Familie', - 'pt-br': 'Família de fontes', - }, - fontFamilyDescription: { - 'en-us': - 'You can specify any font that is on your computer, even if it is not in the list. A comma-separated list of fonts is also supported, where each subsequent font will be used if the previous one is not available.', - 'ru-ru': - 'Вы можете указать любой шрифт, установленный на вашем компьютере, даже если его нет в списке. Также поддерживается список шрифтов, разделённый запятыми, где каждый последующий шрифт будет использоваться, если предыдущий недоступен.', - 'es-es': - 'Puede especificar cualquier fuente de su ordenador, incluso si no está en la lista. También se admite una lista de fuentes separadas por comas, donde se usará cada fuente subsiguiente si la anterior no está disponible.', - 'fr-fr': - "Vous pouvez spécifier n'importe quelle police présente sur votre ordinateur, même si elle ne figure pas dans la liste. Une liste de polices séparées par des virgules est également prise en charge ; chaque police suivante sera utilisée si la précédente n'est pas disponible.", - 'uk-ua': - "Ви можете вказати будь-який шрифт, який є на вашому комп'ютері, навіть якщо його немає в списку. Також підтримується розділений комами список шрифтів, у якому використовуватиметься другий шрифт, якщо перший недоступний тощо.", - 'de-ch': - 'Sie können jede Schriftart angeben, die sich auf Ihrem Computer befindet, auch wenn diese nicht in der Liste enthalten ist. Eine durch Kommas getrennte Liste von Schriftarten wird ebenfalls unterstützt, wobei die zweite Schriftart verwendet wird, wenn die erste nicht verfügbar ist usw.', - 'pt-br': - 'Você pode especificar qualquer fonte que esteja no seu computador, mesmo que ela não esteja na lista. Uma lista de fontes separadas por vírgulas também é suportada, onde cada fonte subsequente será usada se a anterior não estiver disponível.', - }, - defaultFont: { - 'en-us': '(default font)', - 'ru-ru': '(шрифт по умолчанию)', - 'es-es': '(fuente predeterminada)', - 'fr-fr': '(police par défaut)', - 'uk-ua': '(типовий шрифт)', - 'de-ch': '(Standardschriftart)', - 'pt-br': '(fonte padrão)', - }, - maxFormWidth: { - 'en-us': 'Max form width', - 'ru-ru': 'Максимальная ширина формы', - 'es-es': 'Ancho máximo del formulario', - 'fr-fr': 'Largeur maximale du formulaire', - 'uk-ua': 'Максимальна ширина форми', - 'de-ch': 'Maximale Formularbreite', - 'pt-br': 'Largura máxima do formulário', - }, - fieldBackgrounds: { - 'en-us': 'Field backgrounds', - 'ru-ru': 'Фоны полей', - 'es-es': 'Fondos de campo', - 'fr-fr': 'Milieux de terrain', - 'uk-ua': 'Польові фони', - 'de-ch': 'Feldhintergründe', - 'pt-br': 'Fundos de campo', - }, - fieldBackground: { - 'en-us': 'Field background', - 'ru-ru': 'Фон поля', - 'es-es': 'Fondo de campo', - 'fr-fr': 'Contexte du terrain', - 'uk-ua': 'Поле фону', - 'de-ch': 'Feldhintergrund', - 'pt-br': 'Contexto de campo', - }, - disabledFieldBackground: { - 'en-us': 'Disabled field background', - 'ru-ru': 'Отключенный фон поля', - 'es-es': 'Fondo de campo deshabilitado', - 'fr-fr': 'Fond de champ désactivé', - 'uk-ua': 'Вимкнений фон поля', - 'de-ch': 'Deaktivierter Feldhintergrund', - 'pt-br': 'Fundo de campo desativado', - }, - invalidFieldBackground: { - 'en-us': 'Invalid field background', - 'ru-ru': 'Неверный фон поля', - 'es-es': 'Fondo de campo no válido', - 'fr-fr': 'Fond de champ invalide', - 'uk-ua': 'Недійсний фон поля', - 'de-ch': 'Ungültiger Feldhintergrund', - 'pt-br': 'Fundo de campo inválido', - }, - requiredFieldBackground: { - 'en-us': 'Required field background', - 'ru-ru': 'Обязательное поле фон', - 'es-es': 'Fondo del campo obligatorio', - 'fr-fr': 'Contexte du champ obligatoire', - 'uk-ua': "Обов'язковий фон поля", - 'de-ch': 'Feldhintergrund erforderlich', - 'pt-br': 'Histórico de campo obrigatório', - }, - darkFieldBackground: { - 'en-us': 'Field background (dark theme)', - 'ru-ru': 'Фон поля (тёмная тема)', - 'es-es': 'Fondo de campo (tema oscuro)', - 'fr-fr': 'Fond de champ (thème sombre)', - 'uk-ua': 'Фон поля (темна тема)', - 'de-ch': 'Feldhintergrund (Dunkles Thema)', - 'pt-br': 'Fundo de campo (tema escuro)', - }, - darkDisabledFieldBackground: { - 'en-us': 'Disabled field background (dark theme)', - 'ru-ru': 'Отключенный фон поля (тёмная тема)', - 'es-es': 'Fondo de campo deshabilitado (tema oscuro)', - 'fr-fr': 'Fond de champ désactivé (thème sombre)', - 'uk-ua': 'Вимкнений фон поля (темна тема)', - 'de-ch': 'Deaktivierter Feldhintergrund (Dunkles Thema)', - 'pt-br': 'Fundo de campo desativado (tema escuro)', - }, - darkInvalidFieldBackground: { - 'en-us': 'Invalid field background (dark theme)', - 'ru-ru': 'Недопустимый фон поля (тёмная тема)', - 'es-es': 'Fondo de campo no válido (tema oscuro)', - 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', - 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', - 'de-ch': 'Ungültiger Feldhintergrund (Dunkles Thema)', - 'pt-br': 'Fundo de campo inválido (tema escuro)', - }, - darkRequiredFieldBackground: { - 'en-us': 'Required field background (dark theme)', - 'ru-ru': 'Обязательное поле фон (тёмная тема)', - 'es-es': 'Fondo del campo obligatorio (tema oscuro)', - 'fr-fr': 'Fond de champ obligatoire (thème sombre)', - 'uk-ua': 'Обов’язковий фон поля (темна тема)', - 'de-ch': 'Feldhintergrund erforderlich (Dunkles Thema)', - 'pt-br': 'Fundo de campo obrigatório (tema escuro)', - }, - dialogs: { - 'en-us': 'Dialogs', - 'ru-ru': 'Диалоги', - 'es-es': 'Diálogos', - 'fr-fr': 'Boîtes de dialogue', - 'uk-ua': 'Діалоги', - 'de-ch': 'Dialoge', - 'pt-br': 'Diálogos', - }, - appearance: { - 'en-us': 'Appearance', - 'ru-ru': 'Появление', - 'es-es': 'Apariencia', - 'fr-fr': 'Apparence', - 'uk-ua': 'Зовнішній вигляд', - 'de-ch': 'Aussehen', - 'pt-br': 'Aparência', - }, - buttonsLight: { - 'en-us': 'Buttons (light mode)', - 'de-ch': 'Buttons (Helles Thema)', - 'es-es': 'Botones (modo luz)', - 'fr-fr': 'Boutons (mode lumière)', - 'ru-ru': 'Кнопки (световой режим)', - 'uk-ua': 'Кнопки (світлий режим)', - 'pt-br': 'Botões (modo claro)', - }, - buttonsDark: { - 'en-us': 'Buttons (dark mode)', - 'de-ch': 'Buttons (Dunkles Thema)', - 'es-es': 'Botones (modo oscuro)', - 'fr-fr': 'Boutons (mode sombre)', - 'ru-ru': 'Кнопки (темный режим)', - 'uk-ua': 'Кнопки (темний режим)', - 'pt-br': 'Botões (modo escuro)', - }, - translucentDialog: { - 'en-us': 'Translucent dialogs', - 'ru-ru': 'Прозрачные диалоги', - 'es-es': 'Diálogos translúcidos', - 'fr-fr': 'Dialogues translucides', - 'uk-ua': 'Напівпрозорі діалоги', - 'de-ch': 'Durchscheinende Dialoge', - 'pt-br': 'Diálogos translúcidos', - }, - translucentDialogDescription: { - 'en-us': 'Whether dialogs have translucent background.', - 'ru-ru': 'Имеют ли диалоговые окна полупрозрачный фон.', - 'es-es': 'Si los diálogos tienen fondo translúcido.', - 'fr-fr': 'Si les boîtes de dialogue ont un fond translucide.', - 'uk-ua': 'Чи мають діалоги прозорий фон.', - 'de-ch': 'Dialogfenster mit durchscheinenden Hintergrund.', - 'pt-br': 'Se os diálogos têm fundo translúcido.', - }, - alwaysPrompt: { - 'en-us': 'Always prompt to choose collection', - 'ru-ru': 'Всегда предлагайте выбрать коллекцию', - 'es-es': 'Siempre dispuesto a elegir la colección', - 'fr-fr': 'Toujours invité à choisir la collection', - 'uk-ua': 'Завжди підкажуть вибрати колекцію', - 'de-ch': 'Immer zur Auswahl der Sammlung auffordern', - 'pt-br': 'Sempre pronto para escolher a coleção', - }, - treeEditor: { - 'en-us': 'Tree Editor', - 'ru-ru': 'Редактор деревьев', - 'es-es': 'Editor de árboles', - 'fr-fr': "Éditeur d'arborescence", - 'uk-ua': 'Редактор дерева', - 'de-ch': 'Baumeditor', - 'pt-br': 'Editor de Árvore', - }, - treeAccentColor: { - 'en-us': 'Tree accent color', - 'ru-ru': 'Акцентный цвет дерева', - 'es-es': 'Color de acento del árbol', - 'fr-fr': "Couleur d'accent d'arbre", - 'uk-ua': 'Колір акценту дерева', - 'de-ch': 'Baumakzentfarbe', - 'pt-br': 'Cor de destaque da árvore', - }, - synonymColor: { - 'en-us': 'Synonym color', - 'ru-ru': 'Синоним цвет', - 'es-es': 'Color sinónimo', - 'fr-fr': 'Synonyme couleur', - 'uk-ua': 'Синонім кольору', - 'de-ch': 'Synonymfarbe', - 'pt-br': 'Cor sinônimo', - }, - showNewDataSetWarning: { - 'en-us': 'Show new Data Set warning', - 'ru-ru': 'Показать предупреждение о новом наборе данных', - 'es-es': 'Mostrar nueva advertencia de conjunto de datos', - 'fr-fr': "Afficher un nouvel avertissement sur l'ensemble de données", - 'uk-ua': 'Показати попередження про новий набір даних', - 'de-ch': 'Warnung für neuen Datensatz anzeigen', - 'pt-br': 'Mostrar novo aviso de conjunto de dados', - }, - showNewDataSetWarningDescription: { - 'en-us': 'Show an informational message when creating a new Data Set.', - 'ru-ru': - 'Показывать информационное сообщение при создании нового набора данных.', - 'es-es': - 'Mostrar un mensaje informativo al crear un nuevo conjunto de datos.', - 'fr-fr': - "Afficher un message d'information lors de la création d'un nouvel ensemble de données.", - 'uk-ua': - 'Показувати інформаційне повідомлення під час створення нового набору даних.', - 'de-ch': 'Zeige eine Meldung beim erstellen eines neuen Datensatzes an.', - 'pt-br': - 'Exibir uma mensagem informativa ao criar um novo conjunto de dados.', - }, - header: { - 'en-us': 'Navigation Menu', - 'ru-ru': 'Меню навигации', - 'es-es': 'Menú de navegación', - 'fr-fr': 'le menu de navigation', - 'uk-ua': 'Навігаційне меню', - 'de-ch': 'Navigationsmenü', - 'pt-br': 'Menu de navegação', - }, - application: { - 'en-us': 'Application', - 'ru-ru': 'Приложение', - 'es-es': 'Solicitud', - 'fr-fr': 'Application', - 'uk-ua': 'застосування', - 'de-ch': 'Anwendung', - 'pt-br': 'Aplicativo', - }, - allowDismissingErrors: { - 'en-us': 'Allow dismissing error messages', - 'ru-ru': 'Разрешить отклонять сообщения об ошибках', - 'es-es': 'Permitir descartar mensajes de error', - 'fr-fr': "Autoriser le rejet des messages d'erreur", - 'uk-ua': 'Дозволити закривати повідомлення про помилки', - 'de-ch': 'Erlaube das Verwerfen von Fehlermeldungen', - 'pt-br': 'Permitir descartar mensagens de erro', - }, - updatePageTitle: { - 'en-us': 'Update page title', - 'ru-ru': 'Обновить заголовок страницы', - 'es-es': 'Actualizar el título de la página', - 'fr-fr': 'Mettre à jour le titre de la page', - 'uk-ua': 'Оновити назву сторінки', - 'de-ch': 'Seitentitel aktualisieren', - 'pt-br': 'Atualizar título da página', - }, - updatePageTitleDescription: { - 'en-us': - "Whether to update the title of the page to match dialog's header.", - 'ru-ru': - 'Обновлять ли заголовок страницы в соответствии с заголовком диалогового окна.', - 'es-es': - 'Si se debe actualizar el título de la página para que coincida con el encabezado del cuadro de diálogo.', - 'fr-fr': - "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'en-tête de la boîte de dialogue.", - 'uk-ua': - 'Чи оновлювати назву сторінки відповідно до заголовка діалогового вікна.', - 'de-ch': - 'Titel der Seite so aktualisieren, dass er mit der Kopfzeile des Dialogs übereinstimmt.', - 'pt-br': - 'Se o título da página deve ser atualizado para corresponder ao cabeçalho da caixa de diálogo.', - }, - updatePageTitleFormDescription: { - 'en-us': 'Whether to update the title of the page to match current record.', - 'ru-ru': - 'Следует ли обновить заголовок страницы в соответствии с текущей записью.', - 'es-es': - 'Si desea actualizar el título de la página para que coincida con el registro actual.', - 'fr-fr': - "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'enregistrement actuel.", - 'uk-ua': 'Чи оновлювати назву сторінки відповідно до поточного запису.', - 'de-ch': - 'Titel der Seite aktualisieren, damit er mit dem aktuellen Datensatz übereinstimmt.', - 'pt-br': - 'Se o título da página deve ser atualizado para corresponder ao registro atual.', - }, - queryComboBox: { - 'en-us': 'Query Combo Box', - 'ru-ru': 'Поле со списком запросов', - 'es-es': 'Cuadro combinado de consulta', - 'uk-ua': 'Поле зі списком запитів', - 'de-ch': 'Abfrage-Kombinationsfeld', - 'fr-fr': 'Zone de liste déroulante de requête', - 'pt-br': 'Caixa de combinação de consulta', - }, - searchAlgorithm: { - 'en-us': 'Search Algorithm', - 'ru-ru': 'Алгоритм поиска', - 'es-es': 'Algoritmo de búsqueda', - 'fr-fr': 'Algorithme de recherche', - 'uk-ua': 'Алгоритм пошуку', - 'de-ch': 'Suchalgorithmus', - 'pt-br': 'Algoritmo de Busca', - }, - treeSearchAlgorithm: { - 'en-us': 'Search Algorithm (for relationships with tree tables)', - 'ru-ru': 'Алгоритм поиска (для связей с древовидными таблицами)', - 'es-es': 'Algoritmo de búsqueda (para relaciones con tablas de árboles)', - 'fr-fr': - 'Algorithme de recherche (pour les relations avec les tables arborescentes)', - 'uk-ua': 'Алгоритм пошуку (для зв’язків із деревоподібними таблицями)', - 'de-ch': 'Suchalgorithmus (für Beziehungen mit Baumtabellen)', - 'pt-br': 'Algoritmo de busca (para relacionamentos com tabelas de árvore)', - }, - startsWithInsensitive: { - 'en-us': 'Starts With (case-insensitive)', - 'ru-ru': 'Начинается с (без учета регистра)', - 'es-es': 'Comienza con (sin distinguir entre mayúsculas y minúsculas)', - 'fr-fr': 'Commence par (insensible à la casse)', - 'uk-ua': 'Починається з (без урахування регістру)', - 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung wird nicht beachtet)', - 'pt-br': 'Começa com (sem distinção entre maiúsculas e minúsculas)', - }, - startsWithDescription: { - 'en-us': 'Search for values that begin with a given query string.', - 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', - 'es-es': - 'Busque valores que comiencen con una cadena de consulta determinada.', - 'fr-fr': - 'Rechercher des valeurs commençant par une chaîne de requête donnée.', - 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', - 'de-ch': - 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', - 'pt-br': - 'Pesquise valores que começam com uma determinada sequência de consulta.', - }, - startsWithCaseSensitive: { - 'en-us': 'Starts With (case-sensitive)', - 'ru-ru': 'Начинается с (с учетом регистра)', - 'es-es': 'Comienza con (sensible a mayúsculas y minúsculas)', - 'fr-fr': 'Commence par (sensible à la casse)', - 'uk-ua': 'Починається з (з урахуванням регістру)', - 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung beachten)', - 'pt-br': 'Começa com (diferencia maiúsculas de minúsculas)', - }, - startsWithCaseSensitiveDescription: { - 'en-us': 'Search for values that begin with a given query string.', - 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', - 'es-es': - 'Busque valores que comiencen con una cadena de consulta determinada.', - 'fr-fr': - 'Recherchez les valeurs qui commencent par une chaîne de requête donnée.', - 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', - 'de-ch': - 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', - 'pt-br': - 'Pesquise valores que começam com uma determinada sequência de consulta.', - }, - containsInsensitive: { - 'en-us': 'Contains (case-insensitive)', - 'ru-ru': 'Содержит (без учета регистра)', - 'es-es': 'Contiene (sin distinguir entre mayúsculas y minúsculas)', - 'fr-fr': 'Contient (insensible à la casse)', - 'uk-ua': 'Містить (незалежно від регістру)', - 'de-ch': 'Enthält (Groß-/Kleinschreibung wird nicht beachtet)', - 'pt-br': 'Contém (sem distinção entre maiúsculas e minúsculas)', - }, - containsCaseSensitive: { - 'en-us': 'Contains (case-sensitive)', - 'ru-ru': 'Содержит (с учетом регистра)', - 'es-es': 'Contiene (sensible a mayúsculas y minúsculas)', - 'fr-fr': 'Contient (sensible à la casse)', - 'uk-ua': 'Містить (з урахуванням регістру)', - 'de-ch': 'Enthält (Groß-/Kleinschreibung beachten)', - 'pt-br': 'Contém (diferencia maiúsculas de minúsculas)', - }, - containsDescription: { - 'en-us': - 'Search for values that contain a given query string (case-insensitive).', - 'ru-ru': - 'Поиск значений, содержащих заданную строку запроса (без учета регистра).', - 'es-es': - 'Busque valores que contengan una cadena de consulta determinada (sin distinguir entre mayúsculas y minúsculas).', - 'uk-ua': - 'Пошук значень, які містять заданий рядок запиту (незалежно від регістру).', - 'de-ch': - 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (ohne Berücksichtigung der Groß-/Kleinschreibung).', - 'fr-fr': - 'Recherchez les valeurs contenant une chaîne de requête donnée (insensible à la casse).', - 'pt-br': - 'Pesquisar valores que contenham uma determinada sequência de consulta (sem distinção de maiúsculas e minúsculas).', - }, - containsCaseSensitiveDescription: { - 'en-us': - 'Search for values that contain a given query string (case-sensitive).', - 'ru-ru': - 'Поиск значений, содержащих заданную строку запроса (с учетом регистра).', - 'es-es': - 'Busque valores que contengan una cadena de consulta determinada (distingue entre mayúsculas y minúsculas).', - 'fr-fr': - 'Recherchez les valeurs contenant une chaîne de requête donnée (sensible à la casse).', - 'uk-ua': - 'Пошук значень, які містять заданий рядок запиту (з урахуванням регістру).', - 'de-ch': - 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (Groß-/Kleinschreibung beachten).', - 'pt-br': - 'Pesquisar valores que contenham uma determinada sequência de consulta (diferencia maiúsculas de minúsculas).', - }, - containsSecondDescription: { - 'en-us': - 'Can use _ to match any single character or % to match any number of characters.', - 'ru-ru': - 'Можно использовать _ для соответствия любому отдельному символу или % для соответствия любому количеству символов.', - 'es-es': - 'Puede utilizar _ para que coincida con cualquier carácter individual o % para que coincida con cualquier número de caracteres.', - 'fr-fr': - "Peut utiliser _ pour correspondre à n'importe quel caractère ou % pour correspondre à n'importe quel nombre de caractères.", - 'uk-ua': - 'Можна використовувати _ для відповідності будь-якому одному символу або % для відповідності будь-якій кількості символів.', - 'de-ch': - 'Sie können _ verwenden, um ein beliebiges einzelnes Zeichen abzugleichen, oder %, um eine beliebige Anzahl von Zeichen abzugleichen.', - 'pt-br': - 'Pode usar _ para corresponder a qualquer caractere único ou % para corresponder a qualquer número de caracteres.', - }, - highlightMatch: { - 'en-us': 'Highlight matched substring', - 'ru-ru': 'Выделить совпавшую подстроку', - 'es-es': 'Resaltar la subcadena coincidente', - 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', - 'uk-ua': 'Виділіть збіг підрядка', - 'de-ch': 'Markieren Sie übereinstimmende Teilzeichenfolgen', - 'pt-br': 'Destacar substring correspondente', - }, - languageDescription: { - 'en-us': 'Determines field captions, usage notes and table captions.', - 'ru-ru': - 'Определяет заголовки полей, примечания по использованию и заголовки таблиц.', - 'es-es': 'Determina títulos de campos, notas de uso y títulos de tablas.', - 'fr-fr': - "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux.", - 'uk-ua': - 'Визначає підписи полів, примітки щодо використання та підписи таблиць.', - 'de-ch': - 'Legt Feldbeschriftungen, Verwendungshinweise und Tabellenbeschriftungen fest.', - 'pt-br': 'Determina legendas de campo, notas de uso e legendas de tabela.', - }, - showDialogIcon: { - 'en-us': 'Show icon in the header', - 'ru-ru': 'Показывать значок в заголовке', - 'es-es': 'Mostrar icono en el encabezado', - 'fr-fr': "Afficher l'icône dans l'en-tête", - 'uk-ua': 'Показати значок у заголовку', - 'de-ch': 'Symbol in der Kopfzeile anzeigen', - 'pt-br': 'Mostrar ícone no cabeçalho', - }, - scaleInterface: { - 'en-us': 'Scale Interface', - 'ru-ru': 'Интерфейс масштабирования', - 'es-es': 'Interfaz de escala', - 'fr-fr': 'Interface de balance', - 'uk-ua': 'Інтерфейс масштабу', - 'de-ch': 'Waagenschnittstelle', - 'pt-br': 'Interface de escala', - }, - scaleInterfaceDescription: { - 'en-us': 'Scale interface to match font size.', - 'ru-ru': 'Масштабируйте интерфейс в соответствии с размером шрифта.', - 'es-es': 'Escala la interfaz para que coincida con el tamaño de la fuente.', - 'fr-fr': "Adapter l'interface à la taille de la police.", - 'uk-ua': 'Масштабуйте інтерфейс відповідно до розміру шрифту.', - 'de-ch': - 'Skalieren Sie die Benutzeroberfläche, um sie an die Schriftgröße anzupassen.', - 'pt-br': 'Dimensione a interface para corresponder ao tamanho da fonte.', - }, - displayAuthor: { - 'en-us': 'Show author in the tree', - 'ru-ru': 'Показать автора в дереве', - 'es-es': 'Mostrar autor en el árbol', - 'fr-fr': "Afficher l'auteur dans l'arbre", - 'uk-ua': 'Показати автора в дереві', - 'de-ch': 'Autor im Baum anzeigen', - 'pt-br': 'Mostrar autor', - }, - welcomePage: { - 'en-us': 'Home Page', - 'ru-ru': 'Домашняя страница', - 'es-es': 'Página de inicio', - 'fr-fr': "Page d'accueil", - 'uk-ua': 'Домашня сторінка', - 'de-ch': 'Startseite', - 'pt-br': 'Página inicial', - }, - +const preferencesDictionary = { + ...preferencesGeneralDictionary, + ...preferencesContentDictionary, + ...preferencesBehaviorDictionary, } as const; -export const preferencesGeneralDict = createDictionary(preferencesGeneralStrings); - -type PreferencesText = typeof preferencesGeneralDict & - typeof preferencesContentText & - typeof preferencesBehaviorText; - -export const preferencesText = Object.assign( - preferencesGeneralDict, - preferencesContentText, - preferencesBehaviorText -) as PreferencesText; +export const preferencesText = createDictionary(preferencesDictionary); diff --git a/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts b/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts index fe37b54f562..08bb7910ec0 100644 --- a/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts +++ b/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts @@ -12,6 +12,7 @@ import fs from 'node:fs'; import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import { formatConjunction } from '../../components/Atoms/Internationalization'; import { f } from '../../utils/functools'; @@ -76,13 +77,11 @@ export async function extractStrings(): Promise { return undefined; const compiledFilePath = path.join(localizationDirectory, filePath); - const filePathWithoutExtension = compiledFilePath - .split('.') - .slice(0, -1) - .join('.'); - const fileName = filePathWithoutExtension.split('/').at(-1)!; + const fileUrl = pathToFileURL(compiledFilePath).href; + const { name: fileName } = path.parse(compiledFilePath); + if (fileName.includes('.')) return undefined; - const dictionaryFile = await import(filePathWithoutExtension); + const dictionaryFile = await import(fileUrl); const dictionaries = Object.keys(dictionaryFile ?? {}).filter( (dictionaryName) => dictionaryName.endsWith('Text') @@ -168,9 +167,15 @@ export async function scanUsages( { categoryName, strings: Object.fromEntries( - Object.entries(strings).map(([key, strings]) => { - Object.keys(strings) - .filter((key) => !f.has(expectedKeys, key)) + Object.keys(strings).map((key) => { + const rawStrings = + (Reflect.getOwnPropertyDescriptor(strings, key)?.value ?? + (strings as LanguageDictionary)[ + key as keyof LanguageDictionary + ]) as LocalizationEntry; + + Object.keys(rawStrings) + .filter((language) => !f.has(expectedKeys, language)) .forEach((language) => error( [ @@ -189,7 +194,7 @@ export async function scanUsages( ); // Search for blacklisted characters - Object.entries(strings).forEach(([language, string]) => { + Object.entries(rawStrings).forEach(([language, string]) => { if (f.includes(localizationMetaKeys, language)) return; characterBlacklist[language] @@ -211,9 +216,9 @@ export async function scanUsages( key, { strings: { - ...strings, + ...rawStrings, comment: f.maybe( - localized(strings.comment), + localized(rawStrings.comment), whitespaceSensitive ), }, From 7d78c523a096077a301bd9c82be37c60d1c62a49 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 17 Oct 2025 18:17:42 +0000 Subject: [PATCH 063/100] Lint code with ESLint and Prettier Triggered by b1b03396ac63b5e308ecd46143c8c30267665cc5 on branch refs/heads/issue-7440 --- .../lib/localization/preferences.general.ts | 3273 ++++++++--------- .../js_src/lib/localization/preferences.ts | 2 +- .../lib/localization/utils/scanUsages.ts | 8 +- 3 files changed, 1640 insertions(+), 1643 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.general.ts b/specifyweb/frontend/js_src/lib/localization/preferences.general.ts index 30d8adf9225..f17d027dffd 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.general.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.general.ts @@ -7,2086 +7,2085 @@ import { createDictionary } from './utils'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file export const preferencesGeneralDictionary = { preferences: { - "en-us": "Preferences", - "ru-ru": "Настройки", - "es-es": "Preferencias", - "fr-fr": "Préférences", - "uk-ua": "Уподобання", - "de-ch": "Einstellungen", - "pt-br": "Preferências", + 'en-us': 'Preferences', + 'ru-ru': 'Настройки', + 'es-es': 'Preferencias', + 'fr-fr': 'Préférences', + 'uk-ua': 'Уподобання', + 'de-ch': 'Einstellungen', + 'pt-br': 'Preferências', }, customization: { - "en-us": "Customization", - "ru-ru": "Настройка", - "es-es": "Personalización", - "fr-fr": "Personnalisation", - "uk-ua": "Спеціальнізація", - "de-ch": "Anpassung", - "pt-br": "Personalização", + 'en-us': 'Customization', + 'ru-ru': 'Настройка', + 'es-es': 'Personalización', + 'fr-fr': 'Personnalisation', + 'uk-ua': 'Спеціальнізація', + 'de-ch': 'Anpassung', + 'pt-br': 'Personalização', }, userPreferences: { - "en-us": "User Preferences", - "ru-ru": "Настройки пользователя", - "es-es": "Preferencias del usuario", - "fr-fr": "Préférences de l'utilisateur", - "uk-ua": "Налаштування користувача", - "de-ch": "Benutzereinstellungen", - "pt-br": "Preferências do usuário", + 'en-us': 'User Preferences', + 'ru-ru': 'Настройки пользователя', + 'es-es': 'Preferencias del usuario', + 'fr-fr': "Préférences de l'utilisateur", + 'uk-ua': 'Налаштування користувача', + 'de-ch': 'Benutzereinstellungen', + 'pt-br': 'Preferências do usuário', }, defaultUserPreferences: { - "en-us": "Default User Preferences", - "ru-ru": "Настройки пользователя по умолчанию", - "es-es": "Preferencias de usuario predeterminadas", - "fr-fr": "Préférences utilisateur par défaut", - "uk-ua": "Параметри користувача за умовчанням", - "de-ch": "Standardbenutzereinstellungen", - "pt-br": "Preferências de usuário padrão", + 'en-us': 'Default User Preferences', + 'ru-ru': 'Настройки пользователя по умолчанию', + 'es-es': 'Preferencias de usuario predeterminadas', + 'fr-fr': 'Préférences utilisateur par défaut', + 'uk-ua': 'Параметри користувача за умовчанням', + 'de-ch': 'Standardbenutzereinstellungen', + 'pt-br': 'Preferências de usuário padrão', }, general: { - "en-us": "General", - "ru-ru": "Общий", - "es-es": "General", - "fr-fr": "Image personnalisée", - "uk-ua": "Спеціальне зображення", - "de-ch": "Allgemein", - "pt-br": "Em geral", + 'en-us': 'General', + 'ru-ru': 'Общий', + 'es-es': 'General', + 'fr-fr': 'Image personnalisée', + 'uk-ua': 'Спеціальне зображення', + 'de-ch': 'Allgemein', + 'pt-br': 'Em geral', }, ui: { - "en-us": "User Interface", - "ru-ru": "Пользовательский интерфейс", - "es-es": "Interfaz de usuario", - "fr-fr": "Interface utilisateur", - "uk-ua": "Інтерфейс користувача", - "de-ch": "Benutzeroberfläche", - "pt-br": "Interface do usuário", + 'en-us': 'User Interface', + 'ru-ru': 'Пользовательский интерфейс', + 'es-es': 'Interfaz de usuario', + 'fr-fr': 'Interface utilisateur', + 'uk-ua': 'Інтерфейс користувача', + 'de-ch': 'Benutzeroberfläche', + 'pt-br': 'Interface do usuário', }, theme: { - "en-us": "Theme", - "ru-ru": "Тема", - "es-es": "Tema", - "fr-fr": "Thème", - "uk-ua": "Тема", - "de-ch": "Thema", - "pt-br": "Tema", + 'en-us': 'Theme', + 'ru-ru': 'Тема', + 'es-es': 'Tema', + 'fr-fr': 'Thème', + 'uk-ua': 'Тема', + 'de-ch': 'Thema', + 'pt-br': 'Tema', }, useSystemSetting: { - "en-us": "Use system setting", - "ru-ru": "Использовать системные настройки", - "es-es": "Utilizar la configuración del sistema", - "fr-fr": "Utiliser les paramètres du système", - "uk-ua": "Використовуйте налаштування системи", - "de-ch": "Systemeinstellung verwenden", - "pt-br": "Usar configuração do sistema", + 'en-us': 'Use system setting', + 'ru-ru': 'Использовать системные настройки', + 'es-es': 'Utilizar la configuración del sistema', + 'fr-fr': 'Utiliser les paramètres du système', + 'uk-ua': 'Використовуйте налаштування системи', + 'de-ch': 'Systemeinstellung verwenden', + 'pt-br': 'Usar configuração do sistema', }, inheritOsSettings: { - "en-us": "Copies value from your Operating System settings", - "ru-ru": "Копирует значение из настроек вашей операционной системы", - "es-es": "Copia el valor de la configuración de su sistema operativo", - "fr-fr": "Copie la valeur des paramètres de votre système d'exploitation", - "uk-ua": "Копіює значення з налаштувань вашої операційної системи", - "de-ch": "Übernimmt den Wert aus Ihren Betriebssystemeinstellungen", - "pt-br": "Copia o valor das configurações do seu sistema operacional", + 'en-us': 'Copies value from your Operating System settings', + 'ru-ru': 'Копирует значение из настроек вашей операционной системы', + 'es-es': 'Copia el valor de la configuración de su sistema operativo', + 'fr-fr': "Copie la valeur des paramètres de votre système d'exploitation", + 'uk-ua': 'Копіює значення з налаштувань вашої операційної системи', + 'de-ch': 'Übernimmt den Wert aus Ihren Betriebssystemeinstellungen', + 'pt-br': 'Copia o valor das configurações do seu sistema operacional', }, light: { - comment: "Light mode", - "en-us": "Light", - "ru-ru": "Свет", - "es-es": "Luz", - "fr-fr": "Lumière", - "uk-ua": "світло", - "de-ch": "Hell", - "pt-br": "Luz", + comment: 'Light mode', + 'en-us': 'Light', + 'ru-ru': 'Свет', + 'es-es': 'Luz', + 'fr-fr': 'Lumière', + 'uk-ua': 'світло', + 'de-ch': 'Hell', + 'pt-br': 'Luz', }, dark: { - comment: "Dark mode", - "en-us": "Dark", - "ru-ru": "Темный", - "es-es": "Oscuro", - "fr-fr": "Sombre", - "uk-ua": "Темний", - "de-ch": "Dunkel", - "pt-br": "Escuro", + comment: 'Dark mode', + 'en-us': 'Dark', + 'ru-ru': 'Темный', + 'es-es': 'Oscuro', + 'fr-fr': 'Sombre', + 'uk-ua': 'Темний', + 'de-ch': 'Dunkel', + 'pt-br': 'Escuro', }, reduceMotion: { - "en-us": "Reduce motion", - "ru-ru": "Уменьшите движение", - "es-es": "Reducir el movimiento", - "fr-fr": "Réduire les mouvements", - "uk-ua": "Зменшити рух", - "de-ch": "Bewegung reduzieren", - "pt-br": "Reduzir movimento", + 'en-us': 'Reduce motion', + 'ru-ru': 'Уменьшите движение', + 'es-es': 'Reducir el movimiento', + 'fr-fr': 'Réduire les mouvements', + 'uk-ua': 'Зменшити рух', + 'de-ch': 'Bewegung reduzieren', + 'pt-br': 'Reduzir movimento', }, reduceMotionDescription: { - "en-us": "Disable non-essential animations and transitions.", - "ru-ru": "Отключите ненужные анимации и переходы.", - "es-es": "Desactivar animaciones y transiciones no esenciales.", - "fr-fr": "Désactivez les animations et les transitions non essentielles.", - "uk-ua": "Вимкніть необов'язкову анімацію та переходи.", - "de-ch": "Nicht erforderliche Animationen und Übergänge deaktivieren.", - "pt-br": "Desabilite animações e transições não essenciais.", + 'en-us': 'Disable non-essential animations and transitions.', + 'ru-ru': 'Отключите ненужные анимации и переходы.', + 'es-es': 'Desactivar animaciones y transiciones no esenciales.', + 'fr-fr': 'Désactivez les animations et les transitions non essentielles.', + 'uk-ua': "Вимкніть необов'язкову анімацію та переходи.", + 'de-ch': 'Nicht erforderliche Animationen und Übergänge deaktivieren.', + 'pt-br': 'Desabilite animações e transições não essenciais.', }, reduceTransparency: { - "en-us": "Reduce transparency", - "ru-ru": "Уменьшить прозрачность", - "es-es": "Reducir la transparencia", - "fr-fr": "Réduire la transparence", - "uk-ua": "Зменшити прозорість", - "de-ch": "Transparenz reduzieren", - "pt-br": "Reduzir a transparência", + 'en-us': 'Reduce transparency', + 'ru-ru': 'Уменьшить прозрачность', + 'es-es': 'Reducir la transparencia', + 'fr-fr': 'Réduire la transparence', + 'uk-ua': 'Зменшити прозорість', + 'de-ch': 'Transparenz reduzieren', + 'pt-br': 'Reduzir a transparência', }, reduceTransparencyDescription: { - "en-us": - "Whether to disable translucent backgrounds for user interface components whenever possible (e.g. table headers in tree view).", - "ru-ru": - "Следует ли отключать полупрозрачный фон для компонентов пользовательского интерфейса, когда это возможно (например, заголовки таблиц в древовидной структуре).", - "es-es": - "Si se deben deshabilitar los fondos translúcidos para los componentes de la interfaz de usuario siempre que sea posible (por ejemplo, encabezados de tabla en la vista de árbol).", - "fr-fr": + 'en-us': + 'Whether to disable translucent backgrounds for user interface components whenever possible (e.g. table headers in tree view).', + 'ru-ru': + 'Следует ли отключать полупрозрачный фон для компонентов пользовательского интерфейса, когда это возможно (например, заголовки таблиц в древовидной структуре).', + 'es-es': + 'Si se deben deshabilitar los fondos translúcidos para los componentes de la interfaz de usuario siempre que sea posible (por ejemplo, encabezados de tabla en la vista de árbol).', + 'fr-fr': "S'il faut désactiver les arrière-plans translucides pour les composants de l'interface utilisateur chaque fois que possible (par exemple, les en-têtes de tableau dans l'arborescence).", - "uk-ua": - "Чи вимикати напівпрозорий фон для компонентів інтерфейсу користувача, коли це можливо (наприклад, заголовки таблиць у перегляді дерева).", - "de-ch": - "Durchsichtige Hintergründe für Benutzeroberflächenkomponenten wann immer möglich deaktivieren (z. B. Tabellenüberschriften in der Baumansicht).", - "pt-br": - "Se deve ou não desabilitar fundos translúcidos para componentes da interface do usuário sempre que possível (por exemplo, cabeçalhos de tabela na visualização em árvore).", + 'uk-ua': + 'Чи вимикати напівпрозорий фон для компонентів інтерфейсу користувача, коли це можливо (наприклад, заголовки таблиць у перегляді дерева).', + 'de-ch': + 'Durchsichtige Hintergründe für Benutzeroberflächenkomponenten wann immer möglich deaktivieren (z. B. Tabellenüberschriften in der Baumansicht).', + 'pt-br': + 'Se deve ou não desabilitar fundos translúcidos para componentes da interface do usuário sempre que possível (por exemplo, cabeçalhos de tabela na visualização em árvore).', }, contrast: { - "en-us": "Contrast", - "ru-ru": "Контраст", - "es-es": "Contraste", - "fr-fr": "Contraste", - "uk-ua": "Контраст", - "de-ch": "Kontrast", - "pt-br": "Contraste", + 'en-us': 'Contrast', + 'ru-ru': 'Контраст', + 'es-es': 'Contraste', + 'fr-fr': 'Contraste', + 'uk-ua': 'Контраст', + 'de-ch': 'Kontrast', + 'pt-br': 'Contraste', }, increase: { - "en-us": "Increase", - "ru-ru": "Увеличивать", - "es-es": "Aumentar", - "fr-fr": "Augmenter", - "uk-ua": "Збільшити", - "de-ch": "Erhöhen", - "pt-br": "Aumentar", + 'en-us': 'Increase', + 'ru-ru': 'Увеличивать', + 'es-es': 'Aumentar', + 'fr-fr': 'Augmenter', + 'uk-ua': 'Збільшити', + 'de-ch': 'Erhöhen', + 'pt-br': 'Aumentar', }, reduce: { - "en-us": "Reduce", - "ru-ru": "Уменьшать", - "es-es": "Reducir", - "fr-fr": "Réduire", - "uk-ua": "Зменшити", - "de-ch": "Verringern", - "pt-br": "Reduzir", + 'en-us': 'Reduce', + 'ru-ru': 'Уменьшать', + 'es-es': 'Reducir', + 'fr-fr': 'Réduire', + 'uk-ua': 'Зменшити', + 'de-ch': 'Verringern', + 'pt-br': 'Reduzir', }, noPreference: { - "en-us": "No preference", - "ru-ru": "Нет предпочтений", - "es-es": "Sin preferencia", - "fr-fr": "Pas de préférence", - "uk-ua": "Без переваг", - "de-ch": "Keine Präferenz", - "pt-br": "Sem preferência", + 'en-us': 'No preference', + 'ru-ru': 'Нет предпочтений', + 'es-es': 'Sin preferencia', + 'fr-fr': 'Pas de préférence', + 'uk-ua': 'Без переваг', + 'de-ch': 'Keine Präferenz', + 'pt-br': 'Sem preferência', }, fontSize: { - "en-us": "Font size", - "ru-ru": "Размер шрифта", - "es-es": "Tamaño de fuente", - "fr-fr": "Taille de police", - "uk-ua": "Розмір шрифту", - "de-ch": "Schriftgrösse", - "pt-br": "Tamanho da fonte", + 'en-us': 'Font size', + 'ru-ru': 'Размер шрифта', + 'es-es': 'Tamaño de fuente', + 'fr-fr': 'Taille de police', + 'uk-ua': 'Розмір шрифту', + 'de-ch': 'Schriftgrösse', + 'pt-br': 'Tamanho da fonte', }, fontFamily: { - "en-us": "Font family", - "ru-ru": "Семейство шрифтов", - "es-es": "Familia de fuentes", - "fr-fr": "Famille de polices", - "uk-ua": "Сімейство шрифтів", - "de-ch": "Schrift-Familie", - "pt-br": "Família de fontes", + 'en-us': 'Font family', + 'ru-ru': 'Семейство шрифтов', + 'es-es': 'Familia de fuentes', + 'fr-fr': 'Famille de polices', + 'uk-ua': 'Сімейство шрифтів', + 'de-ch': 'Schrift-Familie', + 'pt-br': 'Família de fontes', }, fontFamilyDescription: { - "en-us": - "You can specify any font that is on your computer, even if it is not in the list. A comma-separated list of fonts is also supported, where each subsequent font will be used if the previous one is not available.", - "ru-ru": - "Вы можете указать любой шрифт, установленный на вашем компьютере, даже если его нет в списке. Также поддерживается список шрифтов, разделённый запятыми, где каждый последующий шрифт будет использоваться, если предыдущий недоступен.", - "es-es": - "Puede especificar cualquier fuente de su ordenador, incluso si no está en la lista. También se admite una lista de fuentes separadas por comas, donde se usará cada fuente subsiguiente si la anterior no está disponible.", - "fr-fr": + 'en-us': + 'You can specify any font that is on your computer, even if it is not in the list. A comma-separated list of fonts is also supported, where each subsequent font will be used if the previous one is not available.', + 'ru-ru': + 'Вы можете указать любой шрифт, установленный на вашем компьютере, даже если его нет в списке. Также поддерживается список шрифтов, разделённый запятыми, где каждый последующий шрифт будет использоваться, если предыдущий недоступен.', + 'es-es': + 'Puede especificar cualquier fuente de su ordenador, incluso si no está en la lista. También se admite una lista de fuentes separadas por comas, donde se usará cada fuente subsiguiente si la anterior no está disponible.', + 'fr-fr': "Vous pouvez spécifier n'importe quelle police présente sur votre ordinateur, même si elle ne figure pas dans la liste. Une liste de polices séparées par des virgules est également prise en charge ; chaque police suivante sera utilisée si la précédente n'est pas disponible.", - "uk-ua": + 'uk-ua': "Ви можете вказати будь-який шрифт, який є на вашому комп'ютері, навіть якщо його немає в списку. Також підтримується розділений комами список шрифтів, у якому використовуватиметься другий шрифт, якщо перший недоступний тощо.", - "de-ch": - "Sie können jede Schriftart angeben, die sich auf Ihrem Computer befindet, auch wenn diese nicht in der Liste enthalten ist. Eine durch Kommas getrennte Liste von Schriftarten wird ebenfalls unterstützt, wobei die zweite Schriftart verwendet wird, wenn die erste nicht verfügbar ist usw.", - "pt-br": - "Você pode especificar qualquer fonte que esteja no seu computador, mesmo que ela não esteja na lista. Uma lista de fontes separadas por vírgulas também é suportada, onde cada fonte subsequente será usada se a anterior não estiver disponível.", + 'de-ch': + 'Sie können jede Schriftart angeben, die sich auf Ihrem Computer befindet, auch wenn diese nicht in der Liste enthalten ist. Eine durch Kommas getrennte Liste von Schriftarten wird ebenfalls unterstützt, wobei die zweite Schriftart verwendet wird, wenn die erste nicht verfügbar ist usw.', + 'pt-br': + 'Você pode especificar qualquer fonte que esteja no seu computador, mesmo que ela não esteja na lista. Uma lista de fontes separadas por vírgulas também é suportada, onde cada fonte subsequente será usada se a anterior não estiver disponível.', }, defaultFont: { - "en-us": "(default font)", - "ru-ru": "(шрифт по умолчанию)", - "es-es": "(fuente predeterminada)", - "fr-fr": "(police par défaut)", - "uk-ua": "(типовий шрифт)", - "de-ch": "(Standardschriftart)", - "pt-br": "(fonte padrão)", + 'en-us': '(default font)', + 'ru-ru': '(шрифт по умолчанию)', + 'es-es': '(fuente predeterminada)', + 'fr-fr': '(police par défaut)', + 'uk-ua': '(типовий шрифт)', + 'de-ch': '(Standardschriftart)', + 'pt-br': '(fonte padrão)', }, maxFormWidth: { - "en-us": "Max form width", - "ru-ru": "Максимальная ширина формы", - "es-es": "Ancho máximo del formulario", - "fr-fr": "Largeur maximale du formulaire", - "uk-ua": "Максимальна ширина форми", - "de-ch": "Maximale Formularbreite", - "pt-br": "Largura máxima do formulário", + 'en-us': 'Max form width', + 'ru-ru': 'Максимальная ширина формы', + 'es-es': 'Ancho máximo del formulario', + 'fr-fr': 'Largeur maximale du formulaire', + 'uk-ua': 'Максимальна ширина форми', + 'de-ch': 'Maximale Formularbreite', + 'pt-br': 'Largura máxima do formulário', }, fieldBackgrounds: { - "en-us": "Field backgrounds", - "ru-ru": "Фоны полей", - "es-es": "Fondos de campo", - "fr-fr": "Milieux de terrain", - "uk-ua": "Польові фони", - "de-ch": "Feldhintergründe", - "pt-br": "Fundos de campo", + 'en-us': 'Field backgrounds', + 'ru-ru': 'Фоны полей', + 'es-es': 'Fondos de campo', + 'fr-fr': 'Milieux de terrain', + 'uk-ua': 'Польові фони', + 'de-ch': 'Feldhintergründe', + 'pt-br': 'Fundos de campo', }, fieldBackground: { - "en-us": "Field background", - "ru-ru": "Фон поля", - "es-es": "Fondo de campo", - "fr-fr": "Contexte du terrain", - "uk-ua": "Поле фону", - "de-ch": "Feldhintergrund", - "pt-br": "Contexto de campo", + 'en-us': 'Field background', + 'ru-ru': 'Фон поля', + 'es-es': 'Fondo de campo', + 'fr-fr': 'Contexte du terrain', + 'uk-ua': 'Поле фону', + 'de-ch': 'Feldhintergrund', + 'pt-br': 'Contexto de campo', }, disabledFieldBackground: { - "en-us": "Disabled field background", - "ru-ru": "Отключенный фон поля", - "es-es": "Fondo de campo deshabilitado", - "fr-fr": "Fond de champ désactivé", - "uk-ua": "Вимкнений фон поля", - "de-ch": "Deaktivierter Feldhintergrund", - "pt-br": "Fundo de campo desativado", + 'en-us': 'Disabled field background', + 'ru-ru': 'Отключенный фон поля', + 'es-es': 'Fondo de campo deshabilitado', + 'fr-fr': 'Fond de champ désactivé', + 'uk-ua': 'Вимкнений фон поля', + 'de-ch': 'Deaktivierter Feldhintergrund', + 'pt-br': 'Fundo de campo desativado', }, invalidFieldBackground: { - "en-us": "Invalid field background", - "ru-ru": "Неверный фон поля", - "es-es": "Fondo de campo no válido", - "fr-fr": "Fond de champ invalide", - "uk-ua": "Недійсний фон поля", - "de-ch": "Ungültiger Feldhintergrund", - "pt-br": "Fundo de campo inválido", + 'en-us': 'Invalid field background', + 'ru-ru': 'Неверный фон поля', + 'es-es': 'Fondo de campo no válido', + 'fr-fr': 'Fond de champ invalide', + 'uk-ua': 'Недійсний фон поля', + 'de-ch': 'Ungültiger Feldhintergrund', + 'pt-br': 'Fundo de campo inválido', }, requiredFieldBackground: { - "en-us": "Required field background", - "ru-ru": "Обязательное поле фон", - "es-es": "Antecedentes del campo obligatorio", - "fr-fr": "Contexte du champ obligatoire", - "uk-ua": "Обов'язковий фон поля", - "de-ch": "Feldhintergrund erforderlich", - "pt-br": "Histórico de campo obrigatório", + 'en-us': 'Required field background', + 'ru-ru': 'Обязательное поле фон', + 'es-es': 'Antecedentes del campo obligatorio', + 'fr-fr': 'Contexte du champ obligatoire', + 'uk-ua': "Обов'язковий фон поля", + 'de-ch': 'Feldhintergrund erforderlich', + 'pt-br': 'Histórico de campo obrigatório', }, darkFieldBackground: { - "en-us": "Field background (dark theme)", - "ru-ru": "Фон поля (тёмная тема)", - "es-es": "Fondo de campo (tema oscuro)", - "fr-fr": "Fond de champ (thème sombre)", - "uk-ua": "Фон поля (темна тема)", - "de-ch": "Feldhintergrund (Dunkles Thema)", - "pt-br": "Fundo de campo (tema escuro)", + 'en-us': 'Field background (dark theme)', + 'ru-ru': 'Фон поля (тёмная тема)', + 'es-es': 'Fondo de campo (tema oscuro)', + 'fr-fr': 'Fond de champ (thème sombre)', + 'uk-ua': 'Фон поля (темна тема)', + 'de-ch': 'Feldhintergrund (Dunkles Thema)', + 'pt-br': 'Fundo de campo (tema escuro)', }, darkDisabledFieldBackground: { - "en-us": "Disabled field background (dark theme)", - "ru-ru": "Отключенный фон поля (тёмная тема)", - "es-es": "Fondo de campo deshabilitado (tema oscuro)", - "fr-fr": "Fond de champ désactivé (thème sombre)", - "uk-ua": "Вимкнений фон поля (темна тема)", - "de-ch": "Deaktivierter Feldhintergrund (Dunkles Thema)", - "pt-br": "Fundo de campo desativado (tema escuro)", + 'en-us': 'Disabled field background (dark theme)', + 'ru-ru': 'Отключенный фон поля (тёмная тема)', + 'es-es': 'Fondo de campo deshabilitado (tema oscuro)', + 'fr-fr': 'Fond de champ désactivé (thème sombre)', + 'uk-ua': 'Вимкнений фон поля (темна тема)', + 'de-ch': 'Deaktivierter Feldhintergrund (Dunkles Thema)', + 'pt-br': 'Fundo de campo desativado (tema escuro)', }, darkInvalidFieldBackground: { - "en-us": "Invalid field background (dark theme)", - "ru-ru": "Недопустимый фон поля (тёмная тема)", - "es-es": "Fondo de campo no válido (tema oscuro)", - "fr-fr": "Largeur de colonne de grille de sous-vue flexible", - "uk-ua": "Гнучка ширина стовпця сітки вкладеного перегляду", - "de-ch": "Ungültiger Feldhintergrund (Dunkles Thema)", - "pt-br": "Fundo de campo inválido (tema escuro)", + 'en-us': 'Invalid field background (dark theme)', + 'ru-ru': 'Недопустимый фон поля (тёмная тема)', + 'es-es': 'Fondo de campo no válido (tema oscuro)', + 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', + 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', + 'de-ch': 'Ungültiger Feldhintergrund (Dunkles Thema)', + 'pt-br': 'Fundo de campo inválido (tema escuro)', }, darkRequiredFieldBackground: { - "en-us": "Required field background (dark theme)", - "ru-ru": "Обязательное поле фон (тёмная тема)", - "es-es": "Fondo del campo obligatorio (tema oscuro)", - "fr-fr": "Fond de champ obligatoire (thème sombre)", - "uk-ua": "Обов’язковий фон поля (темна тема)", - "de-ch": "Feldhintergrund erforderlich (Dunkles Thema)", - "pt-br": "Fundo de campo obrigatório (tema escuro)", + 'en-us': 'Required field background (dark theme)', + 'ru-ru': 'Обязательное поле фон (тёмная тема)', + 'es-es': 'Fondo del campo obligatorio (tema oscuro)', + 'fr-fr': 'Fond de champ obligatoire (thème sombre)', + 'uk-ua': 'Обов’язковий фон поля (темна тема)', + 'de-ch': 'Feldhintergrund erforderlich (Dunkles Thema)', + 'pt-br': 'Fundo de campo obrigatório (tema escuro)', }, dialogs: { - "en-us": "Dialogs", - "ru-ru": "Диалоги", - "es-es": "Diálogos", - "fr-fr": "Boîtes de dialogue", - "uk-ua": "Діалоги", - "de-ch": "Dialoge", - "pt-br": "Diálogos", + 'en-us': 'Dialogs', + 'ru-ru': 'Диалоги', + 'es-es': 'Diálogos', + 'fr-fr': 'Boîtes de dialogue', + 'uk-ua': 'Діалоги', + 'de-ch': 'Dialoge', + 'pt-br': 'Diálogos', }, appearance: { - "en-us": "Appearance", - "ru-ru": "Появление", - "es-es": "Apariencia", - "fr-fr": "Apparence", - "uk-ua": "Зовнішній вигляд", - "de-ch": "Aussehen", - "pt-br": "Aparência", + 'en-us': 'Appearance', + 'ru-ru': 'Появление', + 'es-es': 'Apariencia', + 'fr-fr': 'Apparence', + 'uk-ua': 'Зовнішній вигляд', + 'de-ch': 'Aussehen', + 'pt-br': 'Aparência', }, buttonsLight: { - "en-us": "Buttons (light mode)", - "de-ch": "Buttons (Helles Thema)", - "es-es": "Botones (modo luz)", - "fr-fr": "Boutons (mode lumière)", - "ru-ru": "Кнопки (световой режим)", - "uk-ua": "Кнопки (світлий режим)", - "pt-br": "Botões (modo claro)", + 'en-us': 'Buttons (light mode)', + 'de-ch': 'Buttons (Helles Thema)', + 'es-es': 'Botones (modo luz)', + 'fr-fr': 'Boutons (mode lumière)', + 'ru-ru': 'Кнопки (световой режим)', + 'uk-ua': 'Кнопки (світлий режим)', + 'pt-br': 'Botões (modo claro)', }, buttonsDark: { - "en-us": "Buttons (dark mode)", - "de-ch": "Buttons (Dunkles Thema)", - "es-es": "Botones (modo oscuro)", - "fr-fr": "Boutons (mode sombre)", - "ru-ru": "Кнопки (темный режим)", - "uk-ua": "Кнопки (темний режим)", - "pt-br": "Botões (modo escuro)", + 'en-us': 'Buttons (dark mode)', + 'de-ch': 'Buttons (Dunkles Thema)', + 'es-es': 'Botones (modo oscuro)', + 'fr-fr': 'Boutons (mode sombre)', + 'ru-ru': 'Кнопки (темный режим)', + 'uk-ua': 'Кнопки (темний режим)', + 'pt-br': 'Botões (modo escuro)', }, translucentDialog: { - "en-us": "Translucent dialogs", - "ru-ru": "Прозрачные диалоги", - "es-es": "Diálogos translúcidos", - "fr-fr": "Dialogues translucides", - "uk-ua": "Напівпрозорі діалоги", - "de-ch": "Durchscheinende Dialoge", - "pt-br": "Diálogos translúcidos", + 'en-us': 'Translucent dialogs', + 'ru-ru': 'Прозрачные диалоги', + 'es-es': 'Diálogos translúcidos', + 'fr-fr': 'Dialogues translucides', + 'uk-ua': 'Напівпрозорі діалоги', + 'de-ch': 'Durchscheinende Dialoge', + 'pt-br': 'Diálogos translúcidos', }, translucentDialogDescription: { - "en-us": "Whether dialogs have translucent background.", - "ru-ru": "Имеют ли диалоговые окна полупрозрачный фон.", - "es-es": "Si los diálogos tienen fondo translúcido.", - "fr-fr": "Si les boîtes de dialogue ont un fond translucide.", - "uk-ua": "Чи мають діалоги прозорий фон.", - "de-ch": "Dialogfenster mit durchscheinenden Hintergrund.", - "pt-br": "Se os diálogos têm fundo translúcido.", + 'en-us': 'Whether dialogs have translucent background.', + 'ru-ru': 'Имеют ли диалоговые окна полупрозрачный фон.', + 'es-es': 'Si los diálogos tienen fondo translúcido.', + 'fr-fr': 'Si les boîtes de dialogue ont un fond translucide.', + 'uk-ua': 'Чи мають діалоги прозорий фон.', + 'de-ch': 'Dialogfenster mit durchscheinenden Hintergrund.', + 'pt-br': 'Se os diálogos têm fundo translúcido.', }, alwaysPrompt: { - "en-us": "Always prompt to choose collection", - "ru-ru": "Всегда предлагайте выбрать коллекцию", - "es-es": "Siempre dispuesto a elegir la colección", - "fr-fr": "Toujours invité à choisir la collection", - "uk-ua": "Завжди підкажуть вибрати колекцію", - "de-ch": "Immer zur Auswahl der Sammlung auffordern", - "pt-br": "Sempre pronto para escolher a coleção", + 'en-us': 'Always prompt to choose collection', + 'ru-ru': 'Всегда предлагайте выбрать коллекцию', + 'es-es': 'Siempre dispuesto a elegir la colección', + 'fr-fr': 'Toujours invité à choisir la collection', + 'uk-ua': 'Завжди підкажуть вибрати колекцію', + 'de-ch': 'Immer zur Auswahl der Sammlung auffordern', + 'pt-br': 'Sempre pronto para escolher a coleção', }, treeEditor: { - "en-us": "Tree Editor", - "ru-ru": "Редактор деревьев", - "es-es": "Editor de árboles", - "fr-fr": "Éditeur d'arborescence", - "uk-ua": "Редактор дерева", - "de-ch": "Baumeditor", - "pt-br": "Editor de Árvore", + 'en-us': 'Tree Editor', + 'ru-ru': 'Редактор деревьев', + 'es-es': 'Editor de árboles', + 'fr-fr': "Éditeur d'arborescence", + 'uk-ua': 'Редактор дерева', + 'de-ch': 'Baumeditor', + 'pt-br': 'Editor de Árvore', }, treeAccentColor: { - "en-us": "Tree accent color", - "ru-ru": "Акцентный цвет дерева", - "es-es": "Color de acento del árbol", - "fr-fr": "Couleur d'accent d'arbre", - "uk-ua": "Колір акценту дерева", - "de-ch": "Baumakzentfarbe", - "pt-br": "Cor de destaque da árvore", + 'en-us': 'Tree accent color', + 'ru-ru': 'Акцентный цвет дерева', + 'es-es': 'Color de acento del árbol', + 'fr-fr': "Couleur d'accent d'arbre", + 'uk-ua': 'Колір акценту дерева', + 'de-ch': 'Baumakzentfarbe', + 'pt-br': 'Cor de destaque da árvore', }, synonymColor: { - "en-us": "Synonym color", - "ru-ru": "Синоним цвет", - "es-es": "Color sinónimo", - "fr-fr": "Synonyme couleur", - "uk-ua": "Синонім кольору", - "de-ch": "Synonymfarbe", - "pt-br": "Cor sinônimo", + 'en-us': 'Synonym color', + 'ru-ru': 'Синоним цвет', + 'es-es': 'Color sinónimo', + 'fr-fr': 'Synonyme couleur', + 'uk-ua': 'Синонім кольору', + 'de-ch': 'Synonymfarbe', + 'pt-br': 'Cor sinônimo', }, showNewDataSetWarning: { - "en-us": "Show new Data Set warning", - "ru-ru": "Показать предупреждение о новом наборе данных", - "es-es": "Mostrar nueva advertencia de conjunto de datos", - "fr-fr": "Afficher un nouvel avertissement sur l'ensemble de données", - "uk-ua": "Показати попередження про новий набір даних", - "de-ch": "Warnung für neuen Datensatz anzeigen", - "pt-br": "Mostrar novo aviso de conjunto de dados", + 'en-us': 'Show new Data Set warning', + 'ru-ru': 'Показать предупреждение о новом наборе данных', + 'es-es': 'Mostrar nueva advertencia de conjunto de datos', + 'fr-fr': "Afficher un nouvel avertissement sur l'ensemble de données", + 'uk-ua': 'Показати попередження про новий набір даних', + 'de-ch': 'Warnung für neuen Datensatz anzeigen', + 'pt-br': 'Mostrar novo aviso de conjunto de dados', }, showNewDataSetWarningDescription: { - "en-us": "Show an informational message when creating a new Data Set.", - "ru-ru": - "Показывать информационное сообщение при создании нового набора данных.", - "es-es": - "Mostrar un mensaje informativo al crear un nuevo conjunto de datos.", - "fr-fr": + 'en-us': 'Show an informational message when creating a new Data Set.', + 'ru-ru': + 'Показывать информационное сообщение при создании нового набора данных.', + 'es-es': + 'Mostrar un mensaje informativo al crear un nuevo conjunto de datos.', + 'fr-fr': "Afficher un message d'information lors de la création d'un nouvel ensemble de données.", - "uk-ua": - "Показувати інформаційне повідомлення під час створення нового набору даних.", - "de-ch": "Zeige eine Meldung beim erstellen eines neuen Datensatzes an.", - "pt-br": - "Exibir uma mensagem informativa ao criar um novo conjunto de dados.", + 'uk-ua': + 'Показувати інформаційне повідомлення під час створення нового набору даних.', + 'de-ch': 'Zeige eine Meldung beim erstellen eines neuen Datensatzes an.', + 'pt-br': + 'Exibir uma mensagem informativa ao criar um novo conjunto de dados.', }, header: { - "en-us": "Navigation Menu", - "ru-ru": "Меню навигации", - "es-es": "Menú de navegación", - "fr-fr": "le menu de navigation", - "uk-ua": "Навігаційне меню", - "de-ch": "Navigationsmenü", - "pt-br": "Menu de navegação", + 'en-us': 'Navigation Menu', + 'ru-ru': 'Меню навигации', + 'es-es': 'Menú de navegación', + 'fr-fr': 'le menu de navigation', + 'uk-ua': 'Навігаційне меню', + 'de-ch': 'Navigationsmenü', + 'pt-br': 'Menu de navegação', }, application: { - "en-us": "Application", - "ru-ru": "Приложение", - "es-es": "Solicitud", - "fr-fr": "Application", - "uk-ua": "застосування", - "de-ch": "Anwendung", - "pt-br": "Aplicativo", + 'en-us': 'Application', + 'ru-ru': 'Приложение', + 'es-es': 'Solicitud', + 'fr-fr': 'Application', + 'uk-ua': 'застосування', + 'de-ch': 'Anwendung', + 'pt-br': 'Aplicativo', }, allowDismissingErrors: { - "en-us": "Allow dismissing error messages", - "ru-ru": "Разрешить отклонять сообщения об ошибках", - "es-es": "Permitir descartar mensajes de error", - "fr-fr": "Autoriser le rejet des messages d'erreur", - "uk-ua": "Дозволити закривати повідомлення про помилки", - "de-ch": "Erlaube das Verwerfen von Fehlermeldungen", - "pt-br": "Permitir descartar mensagens de erro", + 'en-us': 'Allow dismissing error messages', + 'ru-ru': 'Разрешить отклонять сообщения об ошибках', + 'es-es': 'Permitir descartar mensajes de error', + 'fr-fr': "Autoriser le rejet des messages d'erreur", + 'uk-ua': 'Дозволити закривати повідомлення про помилки', + 'de-ch': 'Erlaube das Verwerfen von Fehlermeldungen', + 'pt-br': 'Permitir descartar mensagens de erro', }, updatePageTitle: { - "en-us": "Update page title", - "ru-ru": "Обновить заголовок страницы", - "es-es": "Actualizar el título de la página", - "fr-fr": "Mettre à jour le titre de la page", - "uk-ua": "Оновити назву сторінки", - "de-ch": "Seitentitel aktualisieren", - "pt-br": "Atualizar título da página", + 'en-us': 'Update page title', + 'ru-ru': 'Обновить заголовок страницы', + 'es-es': 'Actualizar el título de la página', + 'fr-fr': 'Mettre à jour le titre de la page', + 'uk-ua': 'Оновити назву сторінки', + 'de-ch': 'Seitentitel aktualisieren', + 'pt-br': 'Atualizar título da página', }, updatePageTitleDescription: { - "en-us": + 'en-us': "Whether to update the title of the page to match dialog's header.", - "ru-ru": - "Обновлять ли заголовок страницы в соответствии с заголовком диалогового окна.", - "es-es": - "Si se debe actualizar el título de la página para que coincida con el encabezado del cuadro de diálogo.", - "fr-fr": + 'ru-ru': + 'Обновлять ли заголовок страницы в соответствии с заголовком диалогового окна.', + 'es-es': + 'Si se debe actualizar el título de la página para que coincida con el encabezado del cuadro de diálogo.', + 'fr-fr': "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'en-tête de la boîte de dialogue.", - "uk-ua": - "Чи оновлювати назву сторінки відповідно до заголовка діалогового вікна.", - "de-ch": - "Titel der Seite so aktualisieren, dass er mit der Kopfzeile des Dialogs übereinstimmt.", - "pt-br": - "Se o título da página deve ser atualizado para corresponder ao cabeçalho da caixa de diálogo.", + 'uk-ua': + 'Чи оновлювати назву сторінки відповідно до заголовка діалогового вікна.', + 'de-ch': + 'Titel der Seite so aktualisieren, dass er mit der Kopfzeile des Dialogs übereinstimmt.', + 'pt-br': + 'Se o título da página deve ser atualizado para corresponder ao cabeçalho da caixa de diálogo.', }, updatePageTitleFormDescription: { - "en-us": "Whether to update the title of the page to match current record.", - "ru-ru": - "Следует ли обновить заголовок страницы в соответствии с текущей записью.", - "es-es": - "Si desea actualizar el título de la página para que coincida con el registro actual.", - "fr-fr": + 'en-us': 'Whether to update the title of the page to match current record.', + 'ru-ru': + 'Следует ли обновить заголовок страницы в соответствии с текущей записью.', + 'es-es': + 'Si desea actualizar el título de la página para que coincida con el registro actual.', + 'fr-fr': "S'il faut mettre à jour le titre de la page pour qu'il corresponde à l'enregistrement actuel.", - "uk-ua": "Чи оновлювати назву сторінки відповідно до поточного запису.", - "de-ch": - "Titel der Seite aktualisieren, damit er mit dem aktuellen Datensatz übereinstimmt.", - "pt-br": - "Se o título da página deve ser atualizado para corresponder ao registro atual.", + 'uk-ua': 'Чи оновлювати назву сторінки відповідно до поточного запису.', + 'de-ch': + 'Titel der Seite aktualisieren, damit er mit dem aktuellen Datensatz übereinstimmt.', + 'pt-br': + 'Se o título da página deve ser atualizado para corresponder ao registro atual.', }, queryComboBox: { - "en-us": "Query Combo Box", - "ru-ru": "Поле со списком запросов", - "es-es": "Cuadro combinado de consulta", - "uk-ua": "Поле зі списком запитів", - "de-ch": "Abfrage-Kombinationsfeld", - "fr-fr": "Zone de liste déroulante de requête", - "pt-br": "Caixa de combinação de consulta", + 'en-us': 'Query Combo Box', + 'ru-ru': 'Поле со списком запросов', + 'es-es': 'Cuadro combinado de consulta', + 'uk-ua': 'Поле зі списком запитів', + 'de-ch': 'Abfrage-Kombinationsfeld', + 'fr-fr': 'Zone de liste déroulante de requête', + 'pt-br': 'Caixa de combinação de consulta', }, searchAlgorithm: { - "en-us": "Search Algorithm", - "ru-ru": "Алгоритм поиска", - "es-es": "Algoritmo de búsqueda", - "fr-fr": "Algorithme de recherche", - "uk-ua": "Алгоритм пошуку", - "de-ch": "Suchalgorithmus", - "pt-br": "Algoritmo de Busca", + 'en-us': 'Search Algorithm', + 'ru-ru': 'Алгоритм поиска', + 'es-es': 'Algoritmo de búsqueda', + 'fr-fr': 'Algorithme de recherche', + 'uk-ua': 'Алгоритм пошуку', + 'de-ch': 'Suchalgorithmus', + 'pt-br': 'Algoritmo de Busca', }, treeSearchAlgorithm: { - "en-us": "Search Algorithm (for relationships with tree tables)", - "ru-ru": "Алгоритм поиска (для связей с древовидными таблицами)", - "es-es": "Algoritmo de búsqueda (para relaciones con tablas de árbol)", - "fr-fr": - "Algorithme de recherche (pour les relations avec les tables arborescentes)", - "uk-ua": "Алгоритм пошуку (для зв’язків із деревоподібними таблицями)", - "de-ch": "Suchalgorithmus (für Beziehungen mit Baumtabellen)", - "pt-br": "Algoritmo de busca (para relacionamentos com tabelas de árvore)", + 'en-us': 'Search Algorithm (for relationships with tree tables)', + 'ru-ru': 'Алгоритм поиска (для связей с древовидными таблицами)', + 'es-es': 'Algoritmo de búsqueda (para relaciones con tablas de árbol)', + 'fr-fr': + 'Algorithme de recherche (pour les relations avec les tables arborescentes)', + 'uk-ua': 'Алгоритм пошуку (для зв’язків із деревоподібними таблицями)', + 'de-ch': 'Suchalgorithmus (für Beziehungen mit Baumtabellen)', + 'pt-br': 'Algoritmo de busca (para relacionamentos com tabelas de árvore)', }, startsWithInsensitive: { - "en-us": "Starts With (case-insensitive)", - "ru-ru": "Начинается с (без учета регистра)", - "es-es": "Comienza con (sin distinguir entre mayúsculas y minúsculas)", - "fr-fr": "Commence par (insensible à la casse)", - "uk-ua": "Починається з (без урахування регістру)", - "de-ch": "Beginnt mit (Groß-/Kleinschreibung wird nicht beachtet)", - "pt-br": "Começa com (sem distinção de maiúsculas e minúsculas)", + 'en-us': 'Starts With (case-insensitive)', + 'ru-ru': 'Начинается с (без учета регистра)', + 'es-es': 'Comienza con (sin distinguir entre mayúsculas y minúsculas)', + 'fr-fr': 'Commence par (insensible à la casse)', + 'uk-ua': 'Починається з (без урахування регістру)', + 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung wird nicht beachtet)', + 'pt-br': 'Começa com (sem distinção de maiúsculas e minúsculas)', }, startsWithDescription: { - "en-us": "Search for values that begin with a given query string.", - "ru-ru": "Поиск значений, начинающихся с заданной строки запроса.", - "es-es": - "Busque valores que comiencen con una cadena de consulta determinada.", - "fr-fr": - "Rechercher des valeurs commençant par une chaîne de requête donnée.", - "uk-ua": "Пошук значень, які починаються з заданого рядка запиту.", - "de-ch": - "Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.", - "pt-br": - "Pesquisar valores que começam com uma determinada sequência de consulta.", + 'en-us': 'Search for values that begin with a given query string.', + 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', + 'es-es': + 'Busque valores que comiencen con una cadena de consulta determinada.', + 'fr-fr': + 'Rechercher des valeurs commençant par une chaîne de requête donnée.', + 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', + 'de-ch': + 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', + 'pt-br': + 'Pesquisar valores que começam com uma determinada sequência de consulta.', }, startsWithCaseSensitive: { - "en-us": "Starts With (case-sensitive)", - "ru-ru": "Начинается с (с учетом регистра)", - "es-es": "Comienza con (sensible a mayúsculas y minúsculas)", - "fr-fr": "Commence par (sensible à la casse)", - "uk-ua": "Починається з (з урахуванням регістру)", - "de-ch": "Beginnt mit (Groß-/Kleinschreibung beachten)", - "pt-br": "Começa com (diferencia maiúsculas de minúsculas)", + 'en-us': 'Starts With (case-sensitive)', + 'ru-ru': 'Начинается с (с учетом регистра)', + 'es-es': 'Comienza con (sensible a mayúsculas y minúsculas)', + 'fr-fr': 'Commence par (sensible à la casse)', + 'uk-ua': 'Починається з (з урахуванням регістру)', + 'de-ch': 'Beginnt mit (Groß-/Kleinschreibung beachten)', + 'pt-br': 'Começa com (diferencia maiúsculas de minúsculas)', }, startsWithCaseSensitiveDescription: { - "en-us": "Search for values that begin with a given query string.", - "ru-ru": "Поиск значений, начинающихся с заданной строки запроса.", - "es-es": - "Busque valores que comiencen con una cadena de consulta determinada.", - "fr-fr": - "Recherchez les valeurs qui commencent par une chaîne de requête donnée.", - "uk-ua": "Пошук значень, які починаються з заданого рядка запиту.", - "de-ch": - "Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.", - "pt-br": - "Pesquisar valores que começam com uma determinada sequência de consulta.", + 'en-us': 'Search for values that begin with a given query string.', + 'ru-ru': 'Поиск значений, начинающихся с заданной строки запроса.', + 'es-es': + 'Busque valores que comiencen con una cadena de consulta determinada.', + 'fr-fr': + 'Recherchez les valeurs qui commencent par une chaîne de requête donnée.', + 'uk-ua': 'Пошук значень, які починаються з заданого рядка запиту.', + 'de-ch': + 'Suchen Sie nach Werten, die mit einer bestimmten Abfragezeichenfolge beginnen.', + 'pt-br': + 'Pesquisar valores que começam com uma determinada sequência de consulta.', }, containsInsensitive: { - "en-us": "Contains (case-insensitive)", - "ru-ru": "Содержит (без учета регистра)", - "es-es": "Contiene (sin distinguir entre mayúsculas y minúsculas)", - "fr-fr": "Contient (insensible à la casse)", - "uk-ua": "Містить (незалежно від регістру)", - "de-ch": "Enthält (Groß-/Kleinschreibung wird nicht beachtet)", - "pt-br": "Contém (sem distinção entre maiúsculas e minúsculas)", + 'en-us': 'Contains (case-insensitive)', + 'ru-ru': 'Содержит (без учета регистра)', + 'es-es': 'Contiene (sin distinguir entre mayúsculas y minúsculas)', + 'fr-fr': 'Contient (insensible à la casse)', + 'uk-ua': 'Містить (незалежно від регістру)', + 'de-ch': 'Enthält (Groß-/Kleinschreibung wird nicht beachtet)', + 'pt-br': 'Contém (sem distinção entre maiúsculas e minúsculas)', }, containsCaseSensitive: { - "en-us": "Contains (case-sensitive)", - "ru-ru": "Содержит (с учетом регистра)", - "es-es": "Contiene (sensible a mayúsculas y minúsculas)", - "fr-fr": "Contient (sensible à la casse)", - "uk-ua": "Містить (з урахуванням регістру)", - "de-ch": "Enthält (Groß-/Kleinschreibung beachten)", - "pt-br": "Contém (diferencia maiúsculas de minúsculas)", + 'en-us': 'Contains (case-sensitive)', + 'ru-ru': 'Содержит (с учетом регистра)', + 'es-es': 'Contiene (sensible a mayúsculas y minúsculas)', + 'fr-fr': 'Contient (sensible à la casse)', + 'uk-ua': 'Містить (з урахуванням регістру)', + 'de-ch': 'Enthält (Groß-/Kleinschreibung beachten)', + 'pt-br': 'Contém (diferencia maiúsculas de minúsculas)', }, containsDescription: { - "en-us": - "Search for values that contain a given query string (case-insensitive).", - "ru-ru": - "Поиск значений, содержащих заданную строку запроса (без учета регистра).", - "es-es": - "Busque valores que contengan una cadena de consulta determinada (sin distinguir entre mayúsculas y minúsculas).", - "uk-ua": - "Пошук значень, які містять заданий рядок запиту (незалежно від регістру).", - "de-ch": - "Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (ohne Berücksichtigung der Groß-/Kleinschreibung).", - "fr-fr": - "Recherchez les valeurs contenant une chaîne de requête donnée (insensible à la casse).", - "pt-br": - "Pesquisar valores que contenham uma determinada sequência de consulta (sem distinção de maiúsculas e minúsculas).", + 'en-us': + 'Search for values that contain a given query string (case-insensitive).', + 'ru-ru': + 'Поиск значений, содержащих заданную строку запроса (без учета регистра).', + 'es-es': + 'Busque valores que contengan una cadena de consulta determinada (sin distinguir entre mayúsculas y minúsculas).', + 'uk-ua': + 'Пошук значень, які містять заданий рядок запиту (незалежно від регістру).', + 'de-ch': + 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (ohne Berücksichtigung der Groß-/Kleinschreibung).', + 'fr-fr': + 'Recherchez les valeurs contenant une chaîne de requête donnée (insensible à la casse).', + 'pt-br': + 'Pesquisar valores que contenham uma determinada sequência de consulta (sem distinção de maiúsculas e minúsculas).', }, containsCaseSensitiveDescription: { - "en-us": - "Search for values that contain a given query string (case-sensitive).", - "ru-ru": - "Поиск значений, содержащих заданную строку запроса (с учетом регистра).", - "es-es": - "Busque valores que contengan una cadena de consulta determinada (distingue entre mayúsculas y minúsculas).", - "fr-fr": - "Recherchez les valeurs contenant une chaîne de requête donnée (sensible à la casse).", - "uk-ua": - "Пошук значень, які містять заданий рядок запиту (з урахуванням регістру).", - "de-ch": - "Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (Groß-/Kleinschreibung beachten).", - "pt-br": - "Pesquisar valores que contenham uma determinada sequência de consulta (diferencia maiúsculas de minúsculas).", + 'en-us': + 'Search for values that contain a given query string (case-sensitive).', + 'ru-ru': + 'Поиск значений, содержащих заданную строку запроса (с учетом регистра).', + 'es-es': + 'Busque valores que contengan una cadena de consulta determinada (distingue entre mayúsculas y minúsculas).', + 'fr-fr': + 'Recherchez les valeurs contenant une chaîne de requête donnée (sensible à la casse).', + 'uk-ua': + 'Пошук значень, які містять заданий рядок запиту (з урахуванням регістру).', + 'de-ch': + 'Suchen Sie nach Werten, die eine bestimmte Abfragezeichenfolge enthalten (Groß-/Kleinschreibung beachten).', + 'pt-br': + 'Pesquisar valores que contenham uma determinada sequência de consulta (diferencia maiúsculas de minúsculas).', }, containsSecondDescription: { - "en-us": - "Can use _ to match any single character or % to match any number of characters.", - "ru-ru": - "Можно использовать _ для соответствия любому отдельному символу или % для соответствия любому количеству символов.", - "es-es": - "Puede utilizar _ para que coincida con cualquier carácter individual o % para que coincida con cualquier número de caracteres.", - "fr-fr": + 'en-us': + 'Can use _ to match any single character or % to match any number of characters.', + 'ru-ru': + 'Можно использовать _ для соответствия любому отдельному символу или % для соответствия любому количеству символов.', + 'es-es': + 'Puede utilizar _ para que coincida con cualquier carácter individual o % para que coincida con cualquier número de caracteres.', + 'fr-fr': "Peut utiliser _ pour correspondre à n'importe quel caractère ou % pour correspondre à n'importe quel nombre de caractères.", - "uk-ua": - "Можна використовувати _ для відповідності будь-якому одному символу або % для відповідності будь-якій кількості символів.", - "de-ch": - "Sie können _ verwenden, um ein beliebiges einzelnes Zeichen abzugleichen, oder %, um eine beliebige Anzahl von Zeichen abzugleichen.", - "pt-br": - "Pode usar _ para corresponder a qualquer caractere único ou % para corresponder a qualquer número de caracteres.", + 'uk-ua': + 'Можна використовувати _ для відповідності будь-якому одному символу або % для відповідності будь-якій кількості символів.', + 'de-ch': + 'Sie können _ verwenden, um ein beliebiges einzelnes Zeichen abzugleichen, oder %, um eine beliebige Anzahl von Zeichen abzugleichen.', + 'pt-br': + 'Pode usar _ para corresponder a qualquer caractere único ou % para corresponder a qualquer número de caracteres.', }, highlightMatch: { - "en-us": "Highlight matched substring", - "ru-ru": "Выделить совпавшую подстроку", - "es-es": "Resaltar la subcadena coincidente", - "fr-fr": "Mettre en surbrillance la sous-chaîne correspondante", - "uk-ua": "Виділіть збіг підрядка", - "de-ch": "Markieren Sie übereinstimmende Teilzeichenfolgen", - "pt-br": "Destacar substring correspondente", + 'en-us': 'Highlight matched substring', + 'ru-ru': 'Выделить совпавшую подстроку', + 'es-es': 'Resaltar la subcadena coincidente', + 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', + 'uk-ua': 'Виділіть збіг підрядка', + 'de-ch': 'Markieren Sie übereinstimmende Teilzeichenfolgen', + 'pt-br': 'Destacar substring correspondente', }, languageDescription: { - "en-us": "Determines field captions, usage notes and table captions.", - "ru-ru": - "Определяет заголовки полей, примечания по использованию и заголовки таблиц.", - "es-es": "Determina títulos de campos, notas de uso y títulos de tablas.", - "fr-fr": + 'en-us': 'Determines field captions, usage notes and table captions.', + 'ru-ru': + 'Определяет заголовки полей, примечания по использованию и заголовки таблиц.', + 'es-es': 'Determina títulos de campos, notas de uso y títulos de tablas.', + 'fr-fr': "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux.", - "uk-ua": - "Визначає підписи полів, примітки щодо використання та підписи таблиць.", - "de-ch": - "Legt Feldbeschriftungen, Verwendungshinweise und Tabellenbeschriftungen fest.", - "pt-br": "Determina legendas de campo, notas de uso e legendas de tabela.", + 'uk-ua': + 'Визначає підписи полів, примітки щодо використання та підписи таблиць.', + 'de-ch': + 'Legt Feldbeschriftungen, Verwendungshinweise und Tabellenbeschriftungen fest.', + 'pt-br': 'Determina legendas de campo, notas de uso e legendas de tabela.', }, showDialogIcon: { - "en-us": "Show icon in the header", - "ru-ru": "Показывать значок в заголовке", - "es-es": "Mostrar icono en el encabezado", - "fr-fr": "Afficher l'icône dans l'en-tête", - "uk-ua": "Показати значок у заголовку", - "de-ch": "Symbol in der Kopfzeile anzeigen", - "pt-br": "Mostrar ícone no cabeçalho", + 'en-us': 'Show icon in the header', + 'ru-ru': 'Показывать значок в заголовке', + 'es-es': 'Mostrar icono en el encabezado', + 'fr-fr': "Afficher l'icône dans l'en-tête", + 'uk-ua': 'Показати значок у заголовку', + 'de-ch': 'Symbol in der Kopfzeile anzeigen', + 'pt-br': 'Mostrar ícone no cabeçalho', }, scaleInterface: { - "en-us": "Scale Interface", - "ru-ru": "Интерфейс масштабирования", - "es-es": "Interfaz de escala", - "fr-fr": "Interface de balance", - "uk-ua": "Інтерфейс масштабу", - "de-ch": "Waagenschnittstelle", - "pt-br": "Interface de escala", + 'en-us': 'Scale Interface', + 'ru-ru': 'Интерфейс масштабирования', + 'es-es': 'Interfaz de escala', + 'fr-fr': 'Interface de balance', + 'uk-ua': 'Інтерфейс масштабу', + 'de-ch': 'Waagenschnittstelle', + 'pt-br': 'Interface de escala', }, scaleInterfaceDescription: { - "en-us": "Scale interface to match font size.", - "ru-ru": "Масштабируйте интерфейс в соответствии с размером шрифта.", - "es-es": "Escala la interfaz para que coincida con el tamaño de la fuente.", - "fr-fr": "Adapter l'interface à la taille de la police.", - "uk-ua": "Масштабуйте інтерфейс відповідно до розміру шрифту.", - "de-ch": - "Skalieren Sie die Benutzeroberfläche, um sie an die Schriftgröße anzupassen.", - "pt-br": "Dimensione a interface para corresponder ao tamanho da fonte.", + 'en-us': 'Scale interface to match font size.', + 'ru-ru': 'Масштабируйте интерфейс в соответствии с размером шрифта.', + 'es-es': 'Escala la interfaz para que coincida con el tamaño de la fuente.', + 'fr-fr': "Adapter l'interface à la taille de la police.", + 'uk-ua': 'Масштабуйте інтерфейс відповідно до розміру шрифту.', + 'de-ch': + 'Skalieren Sie die Benutzeroberfläche, um sie an die Schriftgröße anzupassen.', + 'pt-br': 'Dimensione a interface para corresponder ao tamanho da fonte.', }, displayAuthor: { - "en-us": "Show author in the tree", - "ru-ru": "Показать автора в дереве", - "es-es": "Mostrar autor en el árbol", - "fr-fr": "Afficher l'auteur dans l'arbre", - "uk-ua": "Показати автора в дереві", - "de-ch": "Autor im Baum anzeigen", - "pt-br": "Mostrar autor", + 'en-us': 'Show author in the tree', + 'ru-ru': 'Показать автора в дереве', + 'es-es': 'Mostrar autor en el árbol', + 'fr-fr': "Afficher l'auteur dans l'arbre", + 'uk-ua': 'Показати автора в дереві', + 'de-ch': 'Autor im Baum anzeigen', + 'pt-br': 'Mostrar autor', }, welcomePage: { - "en-us": "Home Page", - "ru-ru": "Домашняя страница", - "es-es": "Página de inicio", - "fr-fr": "Page d'accueil", - "uk-ua": "Домашня сторінка", - "de-ch": "Startseite", - "pt-br": "Página inicial", + 'en-us': 'Home Page', + 'ru-ru': 'Домашняя страница', + 'es-es': 'Página de inicio', + 'fr-fr': "Page d'accueil", + 'uk-ua': 'Домашня сторінка', + 'de-ch': 'Startseite', + 'pt-br': 'Página inicial', }, content: { - "en-us": "Content", - "ru-ru": "Содержание", - "es-es": "Contenido", - "fr-fr": "Contenu", - "uk-ua": "Зміст", - "de-ch": "Inhalt", - "pt-br": "Contente", + 'en-us': 'Content', + 'ru-ru': 'Содержание', + 'es-es': 'Contenido', + 'fr-fr': 'Contenu', + 'uk-ua': 'Зміст', + 'de-ch': 'Inhalt', + 'pt-br': 'Contente', }, defaultImage: { - "en-us": "Specify Logo", - "ru-ru": "Укажите логотип", - "es-es": "Especificar logotipo", - "fr-fr": "Spécifier le logo", - "uk-ua": "Вкажіть логотип", - "de-ch": "Logo angeben", - "pt-br": "Especificar logotipo", + 'en-us': 'Specify Logo', + 'ru-ru': 'Укажите логотип', + 'es-es': 'Especificar logotipo', + 'fr-fr': 'Spécifier le logo', + 'uk-ua': 'Вкажіть логотип', + 'de-ch': 'Logo angeben', + 'pt-br': 'Especificar logotipo', }, customImage: { - "en-us": "Custom Image", - "ru-ru": "Пользовательское изображение", - "es-es": "Imagen personalizada", - "fr-fr": "Image personnalisée", - "uk-ua": "Спеціальне зображення", - "de-ch": "Benutzerdefiniertes Bild", - "pt-br": "Imagem personalizada", + 'en-us': 'Custom Image', + 'ru-ru': 'Пользовательское изображение', + 'es-es': 'Imagen personalizada', + 'fr-fr': 'Image personnalisée', + 'uk-ua': 'Спеціальне зображення', + 'de-ch': 'Benutzerdefiniertes Bild', + 'pt-br': 'Imagem personalizada', }, embeddedWebpage: { - "en-us": "Embedded web page", - "ru-ru": "Встроенная веб-страница", - "es-es": "Página web incrustada", - "fr-fr": "Page Web intégrée", - "uk-ua": "Вбудована веб-сторінка", - "de-ch": "Eingebettete Webseite", - "pt-br": "Página da web incorporada", + 'en-us': 'Embedded web page', + 'ru-ru': 'Встроенная веб-страница', + 'es-es': 'Página web incrustada', + 'fr-fr': 'Page Web intégrée', + 'uk-ua': 'Вбудована веб-сторінка', + 'de-ch': 'Eingebettete Webseite', + 'pt-br': 'Página da web incorporada', }, embeddedWebpageDescription: { - "en-us": "A URL to a page that would be embedded on the home page:", - "ru-ru": "URL-адрес страницы, которая будет встроена в домашнюю страницу:", - "es-es": "Una URL a una página que se integrará en la página de inicio:", - "fr-fr": "Une URL vers une page qui serait intégrée à la page d'accueil :", - "uk-ua": "URL-адреса сторінки, яка буде вбудована на домашній сторінці:", - "de-ch": - "Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:", - "pt-br": "Um URL para uma página que seria incorporada na página inicial:", + 'en-us': 'A URL to a page that would be embedded on the home page:', + 'ru-ru': 'URL-адрес страницы, которая будет встроена в домашнюю страницу:', + 'es-es': 'Una URL a una página que se integrará en la página de inicio:', + 'fr-fr': "Une URL vers une page qui serait intégrée à la page d'accueil :", + 'uk-ua': 'URL-адреса сторінки, яка буде вбудована на домашній сторінці:', + 'de-ch': + 'Eine URL zu einer Seite, die auf der Startseite eingebettet werden soll:', + 'pt-br': 'Um URL para uma página que seria incorporada na página inicial:', }, behavior: { - "en-us": "Behavior", - "ru-ru": "Поведение", - "es-es": "Comportamiento", - "fr-fr": "Comportement", - "uk-ua": "Поведінка", - "de-ch": "Verhalten", - "pt-br": "Comportamento", + 'en-us': 'Behavior', + 'ru-ru': 'Поведение', + 'es-es': 'Comportamiento', + 'fr-fr': 'Comportement', + 'uk-ua': 'Поведінка', + 'de-ch': 'Verhalten', + 'pt-br': 'Comportamento', }, noRestrictionsMode: { - "en-us": "No restrictions mode", - "ru-ru": "Режим без ограничений", - "es-es": "Modo sin restricciones", - "fr-fr": "Mode sans restriction", - "uk-ua": "Режим без обмежень", - "de-ch": "Modus „Keine Einschränkungen“", - "pt-br": "Modo sem restrições", + 'en-us': 'No restrictions mode', + 'ru-ru': 'Режим без ограничений', + 'es-es': 'Modo sin restricciones', + 'fr-fr': 'Mode sans restriction', + 'uk-ua': 'Режим без обмежень', + 'de-ch': 'Modus „Keine Einschränkungen“', + 'pt-br': 'Modo sem restrições', }, noRestrictionsModeWbDescription: { - "en-us": "Allows uploading data to any field in any table.", - "ru-ru": "Позволяет загружать данные в любое поле любой таблицы.", - "es-es": "Permite cargar datos a cualquier campo de cualquier tabla.", - "fr-fr": + 'en-us': 'Allows uploading data to any field in any table.', + 'ru-ru': 'Позволяет загружать данные в любое поле любой таблицы.', + 'es-es': 'Permite cargar datos a cualquier campo de cualquier tabla.', + 'fr-fr': "Permet de télécharger des données dans n'importe quel champ de n'importe quelle table.", - "uk-ua": "Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.", - "de-ch": - "Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.", - "pt-br": "Permite carregar dados em qualquer campo de qualquer tabela.", + 'uk-ua': 'Дозволяє завантажувати дані в будь-яке поле будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Hochladen von Daten in jedes Feld einer beliebigen Tabelle.', + 'pt-br': 'Permite carregar dados em qualquer campo de qualquer tabela.', }, noRestrictionsModeQueryDescription: { - "en-us": "Allows querying data from any field in any table.", - "ru-ru": "Позволяет запрашивать данные из любого поля любой таблицы.", - "es-es": "Permite consultar datos de cualquier campo de cualquier tabla.", - "fr-fr": + 'en-us': 'Allows querying data from any field in any table.', + 'ru-ru': 'Позволяет запрашивать данные из любого поля любой таблицы.', + 'es-es': 'Permite consultar datos de cualquier campo de cualquier tabla.', + 'fr-fr': "Permet d'interroger les données de n'importe quel champ de n'importe quelle table.", - "uk-ua": "Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.", - "de-ch": - "Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.", - "pt-br": "Permite consultar dados de qualquer campo em qualquer tabela.", + 'uk-ua': 'Дозволяє запитувати дані з будь-якого поля будь-якої таблиці.', + 'de-ch': + 'Ermöglicht das Abfragen von Daten aus jedem Feld in jeder Tabelle.', + 'pt-br': 'Permite consultar dados de qualquer campo em qualquer tabela.', }, noRestrictionsModeWarning: { - "en-us": - "WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.", - "ru-ru": - "ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.", - "es-es": - "ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.", - "uk-ua": - "ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.", - "de-ch": - "WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.", - "fr-fr": + 'en-us': + 'WARNING: enabling this may lead to data loss or database corruption. Please make sure you know what you are doing.', + 'ru-ru': + 'ВНИМАНИЕ: включение этой функции может привести к потере данных или повреждению базы данных. Убедитесь, что вы понимаете, что делаете.', + 'es-es': + 'ADVERTENCIA: Habilitar esta opción podría provocar la pérdida de datos o la corrupción de la base de datos. Asegúrese de saber lo que está haciendo.', + 'uk-ua': + 'ПОПЕРЕДЖЕННЯ: увімкнення цієї функції може призвести до втрати даних або пошкодження бази даних. Переконайтеся, що ви знаєте, що робите.', + 'de-ch': + 'WARNUNG: Das Aktivieren dieser Option kann zu Datenverlust oder Datenbankbeschädigung führen. Bitte stellen Sie sicher, dass Sie wissen, was Sie tun.', + 'fr-fr': "AVERTISSEMENT : l'activation de cette option peut entraîner une perte de données ou une corruption de la base de données. Veuillez vous assurer que vous savez ce que vous faites.", - "pt-br": - "AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.", + 'pt-br': + 'AVISO: habilitar esta opção pode levar à perda de dados ou à corrupção do banco de dados. Certifique-se de saber o que está fazendo.', }, adminsOnlyPreference: { - "en-us": "You don't have permission to change this option", - "ru-ru": "У вас нет разрешения на изменение этой опции.", - "es-es": "No tienes permiso para cambiar esta opción", - "fr-fr": "Vous n'êtes pas autorisé à modifier cette option", - "uk-ua": "Ви не маєте дозволу змінювати цей параметр", - "de-ch": "Sie haben keine Berechtigung, diese Option zu ändern", - "pt-br": "Você não tem permissão para alterar esta opção", + 'en-us': "You don't have permission to change this option", + 'ru-ru': 'У вас нет разрешения на изменение этой опции.', + 'es-es': 'No tienes permiso para cambiar esta opción', + 'fr-fr': "Vous n'êtes pas autorisé à modifier cette option", + 'uk-ua': 'Ви не маєте дозволу змінювати цей параметр', + 'de-ch': 'Sie haben keine Berechtigung, diese Option zu ändern', + 'pt-br': 'Você não tem permissão para alterar esta opção', }, stickyScrolling: { - "en-us": "Sticky scroll bar", - "ru-ru": "Липкая полоса прокрутки", - "es-es": "Barra de desplazamiento fija", - "fr-fr": "Barre de défilement collante", - "uk-ua": "Липка смуга прокрутки", - "de-ch": "Klebrige Bildlaufleiste", - "pt-br": "Barra de rolagem fixa", + 'en-us': 'Sticky scroll bar', + 'ru-ru': 'Липкая полоса прокрутки', + 'es-es': 'Barra de desplazamiento fija', + 'fr-fr': 'Barre de défilement collante', + 'uk-ua': 'Липка смуга прокрутки', + 'de-ch': 'Klebrige Bildlaufleiste', + 'pt-br': 'Barra de rolagem fixa', }, foreground: { - "en-us": "Foreground", - "ru-ru": "Передний план", - "es-es": "Primer plano", - "fr-fr": "Premier plan", - "uk-ua": "Передній план", - "de-ch": "Vordergrund", - "pt-br": "Primeiro plano", + 'en-us': 'Foreground', + 'ru-ru': 'Передний план', + 'es-es': 'Primer plano', + 'fr-fr': 'Premier plan', + 'uk-ua': 'Передній план', + 'de-ch': 'Vordergrund', + 'pt-br': 'Primeiro plano', }, background: { - "en-us": "Background", - "ru-ru": "Фон", - "es-es": "Fondo", - "fr-fr": "Arrière-plan", - "uk-ua": "Фон", - "de-ch": "Hintergrund", - "pt-br": "Fundo", + 'en-us': 'Background', + 'ru-ru': 'Фон', + 'es-es': 'Fondo', + 'fr-fr': 'Arrière-plan', + 'uk-ua': 'Фон', + 'de-ch': 'Hintergrund', + 'pt-br': 'Fundo', }, sidebarTheme: { - "en-us": "Sidebar theme", - "de-ch": "Seitenleistenthema", - "es-es": "Tema de la barra lateral", - "fr-fr": "Thème de la barre latérale", - "ru-ru": "Тема боковой панели", - "uk-ua": "Тема бічної панелі", - "pt-br": "Tema da barra lateral", + 'en-us': 'Sidebar theme', + 'de-ch': 'Seitenleistenthema', + 'es-es': 'Tema de la barra lateral', + 'fr-fr': 'Thème de la barre latérale', + 'ru-ru': 'Тема боковой панели', + 'uk-ua': 'Тема бічної панелі', + 'pt-br': 'Tema da barra lateral', }, darkForeground: { - "en-us": "Foreground (dark theme)", - "ru-ru": "Передний план (тёмная тема)", - "es-es": "Primer plano (tema oscuro)", - "fr-fr": "Premier plan (thème sombre)", - "uk-ua": "Передній план (темна тема)", - "de-ch": "Vordergrund (dunkles Design)", - "pt-br": "Primeiro plano (tema escuro)", + 'en-us': 'Foreground (dark theme)', + 'ru-ru': 'Передний план (тёмная тема)', + 'es-es': 'Primer plano (tema oscuro)', + 'fr-fr': 'Premier plan (thème sombre)', + 'uk-ua': 'Передній план (темна тема)', + 'de-ch': 'Vordergrund (dunkles Design)', + 'pt-br': 'Primeiro plano (tema escuro)', }, darkBackground: { - "en-us": "Background (dark theme)", - "ru-ru": "Фон (тёмная тема)", - "es-es": "Fondo (tema oscuro)", - "fr-fr": "Arrière-plan (thème sombre)", - "uk-ua": "Фон (темна тема)", - "de-ch": "Hintergrund (dunkles Design)", - "pt-br": "Plano de fundo (tema escuro)", + 'en-us': 'Background (dark theme)', + 'ru-ru': 'Фон (тёмная тема)', + 'es-es': 'Fondo (tema oscuro)', + 'fr-fr': 'Arrière-plan (thème sombre)', + 'uk-ua': 'Фон (темна тема)', + 'de-ch': 'Hintergrund (dunkles Design)', + 'pt-br': 'Plano de fundo (tema escuro)', }, accentColor1: { - "en-us": "Accent color 1", - "ru-ru": "Акцентный цвет 1", - "es-es": "Color de acento 1", - "fr-fr": "Couleur d'accent 1", - "uk-ua": "Акцентний колір 1", - "de-ch": "Akzentfarbe 1", - "pt-br": "Cor de destaque 1", + 'en-us': 'Accent color 1', + 'ru-ru': 'Акцентный цвет 1', + 'es-es': 'Color de acento 1', + 'fr-fr': "Couleur d'accent 1", + 'uk-ua': 'Акцентний колір 1', + 'de-ch': 'Akzentfarbe 1', + 'pt-br': 'Cor de destaque 1', }, accentColor2: { - "en-us": "Accent color 2", - "ru-ru": "Акцентный цвет 2", - "es-es": "Color de acento 2", - "fr-fr": "Couleur d'accent 2", - "uk-ua": "Акцентний колір 2", - "de-ch": "Akzentfarbe 2", - "pt-br": "Cor de destaque 2", + 'en-us': 'Accent color 2', + 'ru-ru': 'Акцентный цвет 2', + 'es-es': 'Color de acento 2', + 'fr-fr': "Couleur d'accent 2", + 'uk-ua': 'Акцентний колір 2', + 'de-ch': 'Akzentfarbe 2', + 'pt-br': 'Cor de destaque 2', }, accentColor3: { - "en-us": "Accent color 3", - "ru-ru": "Акцентный цвет 3", - "es-es": "Color de acento 3", - "fr-fr": "Couleur d'accent 3", - "uk-ua": "Акцентний колір 3", - "de-ch": "Akzentfarbe 3", - "pt-br": "Cor de destaque 3", + 'en-us': 'Accent color 3', + 'ru-ru': 'Акцентный цвет 3', + 'es-es': 'Color de acento 3', + 'fr-fr': "Couleur d'accent 3", + 'uk-ua': 'Акцентний колір 3', + 'de-ch': 'Akzentfarbe 3', + 'pt-br': 'Cor de destaque 3', }, accentColor4: { - "en-us": "Accent color 4", - "ru-ru": "Акцентный цвет 4", - "es-es": "Color de acento 4", - "fr-fr": "Couleur d'accent 4", - "uk-ua": "Акцентний колір 4", - "de-ch": "Akzentfarbe 4", - "pt-br": "Cor de destaque 4", + 'en-us': 'Accent color 4', + 'ru-ru': 'Акцентный цвет 4', + 'es-es': 'Color de acento 4', + 'fr-fr': "Couleur d'accent 4", + 'uk-ua': 'Акцентний колір 4', + 'de-ch': 'Akzentfarbe 4', + 'pt-br': 'Cor de destaque 4', }, accentColor5: { - "en-us": "Accent color 5", - "ru-ru": "Акцентный цвет 5", - "es-es": "Color de acento 5", - "fr-fr": "Couleur d'accent 5", - "uk-ua": "Акцентний колір 5", - "de-ch": "Akzentfarbe 5", - "pt-br": "Cor de destaque 5", + 'en-us': 'Accent color 5', + 'ru-ru': 'Акцентный цвет 5', + 'es-es': 'Color de acento 5', + 'fr-fr': "Couleur d'accent 5", + 'uk-ua': 'Акцентний колір 5', + 'de-ch': 'Akzentfarbe 5', + 'pt-br': 'Cor de destaque 5', }, spreadsheet: { - "en-us": "Spreadsheet", - "ru-ru": "Электронная таблица", - "es-es": "Hoja de cálculo", - "fr-fr": "Tableur", - "uk-ua": "Електронна таблиця", - "de-ch": "Kalkulationstabelle", - "pt-br": "Planilha", + 'en-us': 'Spreadsheet', + 'ru-ru': 'Электронная таблица', + 'es-es': 'Hoja de cálculo', + 'fr-fr': 'Tableur', + 'uk-ua': 'Електронна таблиця', + 'de-ch': 'Kalkulationstabelle', + 'pt-br': 'Planilha', }, minSpareRows: { - "en-us": "Number of blank rows at the end", - "ru-ru": "Количество пустых строк в конце", - "es-es": "Número de filas en blanco al final", - "fr-fr": "Nombre de lignes vides à la fin", - "uk-ua": "Кількість порожніх рядків у кінці", - "de-ch": "Anzahl der leeren Zeilen am Ende", - "pt-br": "Número de linhas em branco no final", + 'en-us': 'Number of blank rows at the end', + 'ru-ru': 'Количество пустых строк в конце', + 'es-es': 'Número de filas en blanco al final', + 'fr-fr': 'Nombre de lignes vides à la fin', + 'uk-ua': 'Кількість порожніх рядків у кінці', + 'de-ch': 'Anzahl der leeren Zeilen am Ende', + 'pt-br': 'Número de linhas em branco no final', }, autoWrapCols: { - "en-us": "Navigate to the other side when reaching the edge column", - "ru-ru": "Достигнув крайней колонны, перейдите на другую сторону.", - "es-es": "Navegue hacia el otro lado al llegar a la columna del borde.", - "fr-fr": - "Naviguez de l’autre côté lorsque vous atteignez la colonne de bord", - "uk-ua": "Перейдіть на іншу сторону, коли досягнете краю колонки", - "de-ch": - "Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen", - "pt-br": "Navegue para o outro lado ao atingir a coluna da borda", + 'en-us': 'Navigate to the other side when reaching the edge column', + 'ru-ru': 'Достигнув крайней колонны, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la columna del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la colonne de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете краю колонки', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randspalte erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a coluna da borda', }, autoWrapRows: { - "en-us": "Navigate to the other side when reaching the edge row", - "ru-ru": "Достигнув крайнего ряда, перейдите на другую сторону.", - "es-es": "Navegue hacia el otro lado al llegar a la fila del borde.", - "fr-fr": - "Naviguez de l’autre côté lorsque vous atteignez la rangée de bord", - "uk-ua": "Перейдіть на іншу сторону, коли досягнете крайнього ряду", - "de-ch": - "Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen", - "pt-br": "Navegue para o outro lado ao atingir a fileira de bordas", + 'en-us': 'Navigate to the other side when reaching the edge row', + 'ru-ru': 'Достигнув крайнего ряда, перейдите на другую сторону.', + 'es-es': 'Navegue hacia el otro lado al llegar a la fila del borde.', + 'fr-fr': + 'Naviguez de l’autre côté lorsque vous atteignez la rangée de bord', + 'uk-ua': 'Перейдіть на іншу сторону, коли досягнете крайнього ряду', + 'de-ch': + 'Navigieren Sie zur anderen Seite, wenn Sie die Randreihe erreichen', + 'pt-br': 'Navegue para o outro lado ao atingir a fileira de bordas', }, enterBeginsEditing: { - "en-us": "Enter key begins editing cell", - "ru-ru": "Клавиша Enter начинает редактирование ячейки.", - "es-es": "La tecla Enter inicia la edición de la celda", - "fr-fr": "La touche Entrée commence à modifier la cellule", - "uk-ua": "Клавіша Enter починає редагування клітинки", - "de-ch": "Mit der Eingabetaste beginnt die Bearbeitung der Zelle", - "pt-br": "A tecla Enter inicia a edição da célula", + 'en-us': 'Enter key begins editing cell', + 'ru-ru': 'Клавиша Enter начинает редактирование ячейки.', + 'es-es': 'La tecla Enter inicia la edición de la celda', + 'fr-fr': 'La touche Entrée commence à modifier la cellule', + 'uk-ua': 'Клавіша Enter починає редагування клітинки', + 'de-ch': 'Mit der Eingabetaste beginnt die Bearbeitung der Zelle', + 'pt-br': 'A tecla Enter inicia a edição da célula', }, tabMoveDirection: { - "en-us": "Direction of movement when Tab key is pressed", - "ru-ru": "Направление движения при нажатии клавиши Tab", - "es-es": - "Dirección de movimiento cuando se presiona la tecla Tab", - "fr-fr": - "Sens de déplacement lorsque la touche Tabulation est enfoncée", - "uk-ua": "Напрямок руху при натисканні клавіші Tab", - "de-ch": "Bewegungsrichtung beim Drücken der Tab-Taste", - "pt-br": "Direção do movimento quando a tecla Tab é pressionada", + 'en-us': 'Direction of movement when Tab key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Tab', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Tab', + 'fr-fr': + 'Sens de déplacement lorsque la touche Tabulation est enfoncée', + 'uk-ua': 'Напрямок руху при натисканні клавіші Tab', + 'de-ch': 'Bewegungsrichtung beim Drücken der Tab-Taste', + 'pt-br': 'Direção do movimento quando a tecla Tab é pressionada', }, tabMoveDirectionDescription: { - "en-us": - "You can move in the opposite direction by pressing Shift+Tab.", - "ru-ru": - "Вы можете двигаться в обратном направлении, нажав Shift+Tab.", - "es-es": - "Puedes moverte en la dirección opuesta presionando Shift+Tab.", - "fr-fr": - "Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.", - "uk-ua": - "Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.", - "de-ch": - "Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.", - "pt-br": - "Você pode mover na direção oposta pressionando Shift+Tab.", + 'en-us': + 'You can move in the opposite direction by pressing Shift+Tab.', + 'ru-ru': + 'Вы можете двигаться в обратном направлении, нажав Shift+Tab.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Tab.', + 'fr-fr': + 'Vous pouvez vous déplacer dans la direction opposée en appuyant sur Shift+Tab.', + 'uk-ua': + 'Ви можете рухатися в протилежному напрямку, натискаючи Shift+Tab.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Tab drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Tab.', }, column: { - "en-us": "Column", - "ru-ru": "Столбец", - "es-es": "Columna", - "fr-fr": "Colonne", - "uk-ua": "Колонка", - "de-ch": "Spalte", - "pt-br": "Coluna", + 'en-us': 'Column', + 'ru-ru': 'Столбец', + 'es-es': 'Columna', + 'fr-fr': 'Colonne', + 'uk-ua': 'Колонка', + 'de-ch': 'Spalte', + 'pt-br': 'Coluna', }, row: { - "en-us": "Row", - "ru-ru": "Ряд", - "es-es": "Fila", - "fr-fr": "Rangée", - "uk-ua": "рядок", - "de-ch": "Reihe", - "pt-br": "Linha", + 'en-us': 'Row', + 'ru-ru': 'Ряд', + 'es-es': 'Fila', + 'fr-fr': 'Rangée', + 'uk-ua': 'рядок', + 'de-ch': 'Reihe', + 'pt-br': 'Linha', }, enterMoveDirection: { - "en-us": "Direction of movement when Enter key is pressed", - "ru-ru": "Направление движения при нажатии клавиши Enter", - "es-es": - "Dirección de movimiento cuando se presiona la tecla Enter", - "uk-ua": "Напрямок руху, коли натиснуто клавішу Enter", - "de-ch": "Bewegungsrichtung beim Drücken der Taste Enter", - "fr-fr": - "Direction du mouvement lorsque la touche Entrer est enfoncée", - "pt-br": - "Direção do movimento quando a tecla Enter é pressionada", + 'en-us': 'Direction of movement when Enter key is pressed', + 'ru-ru': 'Направление движения при нажатии клавиши Enter', + 'es-es': + 'Dirección de movimiento cuando se presiona la tecla Enter', + 'uk-ua': 'Напрямок руху, коли натиснуто клавішу Enter', + 'de-ch': 'Bewegungsrichtung beim Drücken der Taste Enter', + 'fr-fr': + 'Direction du mouvement lorsque la touche Entrer est enfoncée', + 'pt-br': + 'Direção do movimento quando a tecla Enter é pressionada', }, enterMoveDirectionDescription: { - "en-us": - "You can move in the opposite direction by pressing Shift+Enter.", - "ru-ru": - "Вы можете двигаться в противоположном направлении, нажав Shift+Enter.", - "es-es": - "Puedes moverte en la dirección opuesta presionando Shift+Enter.", - "fr-fr": "Synonyme couleur.", - "uk-ua": - "Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.", - "de-ch": - "Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.", - "pt-br": - "Você pode mover na direção oposta pressionando Shift+Enter.", + 'en-us': + 'You can move in the opposite direction by pressing Shift+Enter.', + 'ru-ru': + 'Вы можете двигаться в противоположном направлении, нажав Shift+Enter.', + 'es-es': + 'Puedes moverte en la dirección opuesta presionando Shift+Enter.', + 'fr-fr': 'Synonyme couleur.', + 'uk-ua': + 'Ви можете рухатися у протилежному напрямку, натискаючи Shift+Enter.', + 'de-ch': + 'Sie können sich in die entgegengesetzte Richtung bewegen, indem Sie Umschalt+Eingabe drücken.', + 'pt-br': + 'Você pode mover na direção oposta pressionando Shift+Enter.', }, filterPickLists: { - "en-us": "Filter pick list items", - "ru-ru": "Фильтрация элементов списка выбора", - "es-es": "Filtrar elementos de la lista de selección", - "fr-fr": "Filtrer les éléments de la liste de sélection", - "uk-ua": "Фільтр вибору елементів списку", - "de-ch": "Auswahllistenelemente filtern", - "pt-br": "Filtrar itens da lista de seleção", + 'en-us': 'Filter pick list items', + 'ru-ru': 'Фильтрация элементов списка выбора', + 'es-es': 'Filtrar elementos de la lista de selección', + 'fr-fr': 'Filtrer les éléments de la liste de sélection', + 'uk-ua': 'Фільтр вибору елементів списку', + 'de-ch': 'Auswahllistenelemente filtern', + 'pt-br': 'Filtrar itens da lista de seleção', }, exportFileDelimiter: { - "en-us": "Export file delimiter", - "ru-ru": "Разделитель файлов экспорта", - "es-es": "Delimitador de archivo de exportación", - "fr-fr": "Délimiteur de fichier d'exportation", - "uk-ua": "Роздільник файлу експорту", - "de-ch": "Dateitrennzeichen exportieren", - "pt-br": "Delimitador de arquivo de exportação", + 'en-us': 'Export file delimiter', + 'ru-ru': 'Разделитель файлов экспорта', + 'es-es': 'Delimitador de archivo de exportación', + 'fr-fr': "Délimiteur de fichier d'exportation", + 'uk-ua': 'Роздільник файлу експорту', + 'de-ch': 'Dateitrennzeichen exportieren', + 'pt-br': 'Delimitador de arquivo de exportação', }, exportCsvUtf8Bom: { - "en-us": "Add UTF-8 BOM to CSV file exports", - "ru-ru": "Добавить UTF-8 BOM в экспорт CSV-файла", - "es-es": "Agregar BOM UTF-8 a las exportaciones de archivos CSV", - "fr-fr": "Ajouter UTF-8 BOM aux exportations de fichiers CSV", - "uk-ua": "Додайте специфікацію UTF-8 до експорту файлу CSVу", - "de-ch": "UTF-8 BOM zum CSV-Dateiexport hinzufügen", - "pt-br": "Adicionar UTF-8 BOM às exportações de arquivos CSV", + 'en-us': 'Add UTF-8 BOM to CSV file exports', + 'ru-ru': 'Добавить UTF-8 BOM в экспорт CSV-файла', + 'es-es': 'Agregar BOM UTF-8 a las exportaciones de archivos CSV', + 'fr-fr': 'Ajouter UTF-8 BOM aux exportations de fichiers CSV', + 'uk-ua': 'Додайте специфікацію UTF-8 до експорту файлу CSVу', + 'de-ch': 'UTF-8 BOM zum CSV-Dateiexport hinzufügen', + 'pt-br': 'Adicionar UTF-8 BOM às exportações de arquivos CSV', }, exportCsvUtf8BomDescription: { - "en-us": - "Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.", - "ru-ru": "Корректное отображение экспортированных CSV-файлов в Excel.", - "es-es": - "Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.", - "fr-fr": + 'en-us': + 'Adds a BOM (Byte Order Mark) to exported CSV files to ensure that the file is correctly recognized and displayed by various programs (Excel, OpenRefine, etc.), preventing issues with special characters and formatting.', + 'ru-ru': 'Корректное отображение экспортированных CSV-файлов в Excel.', + 'es-es': + 'Agrega una BOM (marca de orden de bytes) a los archivos CSV exportados para garantizar que el archivo sea reconocido y mostrado correctamente por varios programas (Excel, OpenRefine, etc.), evitando problemas con caracteres especiales y formato.', + 'fr-fr': "Permet aux exportations de fichiers CSV de s'afficher correctement dans Excel.", - "uk-ua": "Змушує експорт файлів CSV правильно відображатися в Excel.", - "de-ch": - "Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.", - "pt-br": - "Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.", + 'uk-ua': 'Змушує експорт файлів CSV правильно відображатися в Excel.', + 'de-ch': + 'Sorgt dafür, dass CSV-Dateiexporte in Excel korrekt angezeigt werden.', + 'pt-br': + 'Adiciona uma BOM (Byte Order Mark) aos arquivos CSV exportados para garantir que o arquivo seja reconhecido e exibido corretamente por vários programas (Excel, OpenRefine, etc.), evitando problemas com caracteres especiais e formatação.', }, caseSensitive: { - "en-us": "Case-sensitive", - "ru-ru": "С учетом регистра", - "es-es": "Distingue mayúsculas y minúsculas", - "fr-fr": "Sensible aux majuscules et minuscules", - "uk-ua": "Чутливий до регістру", - "de-ch": "Groß- und Kleinschreibung beachten", - "pt-br": "Maiúsculas e minúsculas", + 'en-us': 'Case-sensitive', + 'ru-ru': 'С учетом регистра', + 'es-es': 'Distingue mayúsculas y minúsculas', + 'fr-fr': 'Sensible aux majuscules et minuscules', + 'uk-ua': 'Чутливий до регістру', + 'de-ch': 'Groß- und Kleinschreibung beachten', + 'pt-br': 'Maiúsculas e minúsculas', }, caseInsensitive: { - "en-us": "Case-insensitive", - "ru-ru": "Без учета регистра", - "es-es": "Sin distinción entre mayúsculas y minúsculas", - "fr-fr": "Insensible à la casse", - "uk-ua": "Регістр не враховується", - "de-ch": "Groß-/Kleinschreibung wird nicht beachtet", - "pt-br": "Não diferencia maiúsculas de minúsculas", + 'en-us': 'Case-insensitive', + 'ru-ru': 'Без учета регистра', + 'es-es': 'Sin distinción entre mayúsculas y minúsculas', + 'fr-fr': 'Insensible à la casse', + 'uk-ua': 'Регістр не враховується', + 'de-ch': 'Groß-/Kleinschreibung wird nicht beachtet', + 'pt-br': 'Não diferencia maiúsculas de minúsculas', }, showNoReadTables: { - "en-us": 'Show tables without "Read" access', - "ru-ru": "Показывать таблицы без доступа «Чтение»", - "es-es": 'Mostrar tablas sin acceso de "Lectura"', - "fr-fr": 'Afficher les tableaux sans accès "Lecture"', - "uk-ua": "Показувати таблиці без доступу «Читання»", - "de-ch": "Tabellen ohne Lesezugriff anzeigen", - "pt-br": 'Mostrar tabelas sem acesso de "Leitura"', + 'en-us': 'Show tables without "Read" access', + 'ru-ru': 'Показывать таблицы без доступа «Чтение»', + 'es-es': 'Mostrar tablas sin acceso de "Lectura"', + 'fr-fr': 'Afficher les tableaux sans accès "Lecture"', + 'uk-ua': 'Показувати таблиці без доступу «Читання»', + 'de-ch': 'Tabellen ohne Lesezugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso de "Leitura"', }, showNoAccessTables: { - "en-us": 'Show tables without "Create" access', - "ru-ru": "Показывать таблицы без права «Создать»", - "es-es": 'Mostrar tablas sin acceso "Crear"', - "fr-fr": 'Afficher les tableaux sans accès "Créer"', - "uk-ua": "Показувати таблиці без доступу «Створити»", - "de-ch": "Tabellen ohne „Erstellen“-Zugriff anzeigen", - "pt-br": 'Mostrar tabelas sem acesso "Criar"', + 'en-us': 'Show tables without "Create" access', + 'ru-ru': 'Показывать таблицы без права «Создать»', + 'es-es': 'Mostrar tablas sin acceso "Crear"', + 'fr-fr': 'Afficher les tableaux sans accès "Créer"', + 'uk-ua': 'Показувати таблиці без доступу «Створити»', + 'de-ch': 'Tabellen ohne „Erstellen“-Zugriff anzeigen', + 'pt-br': 'Mostrar tabelas sem acesso "Criar"', }, textAreaAutoGrow: { - "en-us": "Text boxes grow automatically", - "ru-ru": "Текстовые поля увеличиваются автоматически", - "es-es": "Los cuadros de texto crecen automáticamente", - "fr-fr": "Les zones de texte s'agrandissent automatiquement", - "uk-ua": "Текстові поля збільшуються автоматично", - "de-ch": "Textfelder werden automatisch vergrößert", - "pt-br": "As caixas de texto crescem automaticamente", + 'en-us': 'Text boxes grow automatically', + 'ru-ru': 'Текстовые поля увеличиваются автоматически', + 'es-es': 'Los cuadros de texto crecen automáticamente', + 'fr-fr': "Les zones de texte s'agrandissent automatiquement", + 'uk-ua': 'Текстові поля збільшуються автоматично', + 'de-ch': 'Textfelder werden automatisch vergrößert', + 'pt-br': 'As caixas de texto crescem automaticamente', }, clearQueryFilters: { - "en-us": "Reset query filters", - "ru-ru": "Сбросить фильтры запроса", - "es-es": "Restablecer filtros de consulta", - "fr-fr": "Réinitialiser les filtres de requête", - "uk-ua": "Скинути фільтри запитів", - "de-ch": "Abfragefilter zurücksetzen", - "pt-br": "Redefinir filtros de consulta", + 'en-us': 'Reset query filters', + 'ru-ru': 'Сбросить фильтры запроса', + 'es-es': 'Restablecer filtros de consulta', + 'fr-fr': 'Réinitialiser les filtres de requête', + 'uk-ua': 'Скинути фільтри запитів', + 'de-ch': 'Abfragefilter zurücksetzen', + 'pt-br': 'Redefinir filtros de consulta', }, clearQueryFiltersDescription: { - "en-us": "Clears all query filters when running a Report from a Form.", - "de-ch": - "Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.", - "es-es": - "Borra todos los filtros de consulta al ejecutar un informe desde un formulario.", - "fr-fr": + 'en-us': 'Clears all query filters when running a Report from a Form.', + 'de-ch': + 'Löscht alle Abfragefilter, wenn ein Bericht aus einem Formular ausgeführt wird.', + 'es-es': + 'Borra todos los filtros de consulta al ejecutar un informe desde un formulario.', + 'fr-fr': "Efface tous les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire.", - "ru-ru": "Очищает все фильтры запроса при запуске отчета из формы.", - "uk-ua": "Очищає всі фільтри запитів під час запуску звіту з форми.", - "pt-br": - "Limpa todos os filtros de consulta ao executar um relatório de um formulário.", + 'ru-ru': 'Очищает все фильтры запроса при запуске отчета из формы.', + 'uk-ua': 'Очищає всі фільтри запитів під час запуску звіту з форми.', + 'pt-br': + 'Limpa todos os filtros de consulta ao executar um relatório de um formulário.', }, queryParamtersFromForm: { - "en-us": "Show query filters when running a Report from a Form", - "de-ch": - "Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird", - "es-es": - "Mostrar filtros de consulta al ejecutar un informe desde un formulario", - "fr-fr": + 'en-us': 'Show query filters when running a Report from a Form', + 'de-ch': + 'Abfragefilter anzeigen, wenn ein Bericht aus einem Formular ausgeführt wird', + 'es-es': + 'Mostrar filtros de consulta al ejecutar un informe desde un formulario', + 'fr-fr': "Afficher les filtres de requête lors de l'exécution d'un rapport à partir d'un formulaire", - "ru-ru": "Показывать фильтры запроса при запуске отчета из формы", - "uk-ua": "Показувати фільтри запитів під час запуску звіту з форми", - "pt-br": - "Mostrar filtros de consulta ao executar um relatório de um formulário", + 'ru-ru': 'Показывать фильтры запроса при запуске отчета из формы', + 'uk-ua': 'Показувати фільтри запитів під час запуску звіту з форми', + 'pt-br': + 'Mostrar filtros de consulta ao executar um relatório de um formulário', }, autoGrowAutoComplete: { - "en-us": "Allow autocomplete to grow as wide as need", - "ru-ru": - "Разрешить автозаполнению расширяться настолько, насколько это необходимо", - "es-es": "Permitir que el autocompletado crezca tanto como sea necesario", - "fr-fr": - "Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée", - "uk-ua": - "Дозволити автозаповнення розширюватися настільки, наскільки потрібно", - "de-ch": - "Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen", - "pt-br": - "Permitir que o preenchimento automático cresça o quanto for necessário", + 'en-us': 'Allow autocomplete to grow as wide as need', + 'ru-ru': + 'Разрешить автозаполнению расширяться настолько, насколько это необходимо', + 'es-es': 'Permitir que el autocompletado crezca tanto como sea necesario', + 'fr-fr': + 'Sens de déplacement lorsque la touche [X27X]Tabulation[X35X] est enfoncée', + 'uk-ua': + 'Дозволити автозаповнення розширюватися настільки, наскільки потрібно', + 'de-ch': + 'Erlauben Sie der Autovervollständigung, so weit wie nötig zu wachsen', + 'pt-br': + 'Permitir que o preenchimento automático cresça o quanto for necessário', }, tableNameInTitle: { - "en-us": "Include table name in the browser page title", - "ru-ru": "Включить имя таблицы в заголовок страницы браузера", - "es-es": - "Incluir el nombre de la tabla en el título de la página del navegador", - "fr-fr": - "Inclure le nom de la table dans le titre de la page du navigateur", - "uk-ua": "Включіть назву таблиці в заголовок сторінки браузера", - "de-ch": "Tabellennamen in den Seitentitel des Browsers aufnehmen", - "pt-br": "Incluir nome da tabela no título da página do navegador", + 'en-us': 'Include table name in the browser page title', + 'ru-ru': 'Включить имя таблицы в заголовок страницы браузера', + 'es-es': + 'Incluir el nombre de la tabla en el título de la página del navegador', + 'fr-fr': + 'Inclure le nom de la table dans le titre de la page du navigateur', + 'uk-ua': 'Включіть назву таблиці в заголовок сторінки браузера', + 'de-ch': 'Tabellennamen in den Seitentitel des Browsers aufnehmen', + 'pt-br': 'Incluir nome da tabela no título da página do navegador', }, focusFirstField: { - "en-us": "Focus first field", - "de-ch": "Fokus erstes Feld", - "es-es": "Enfoque el primer campo", - "fr-fr": "Concentrez-vous sur le premier champ", - "ru-ru": "Фокус первого поля", - "uk-ua": "Перейти до першого поля", - "pt-br": "Foco primeiro no campo", + 'en-us': 'Focus first field', + 'de-ch': 'Fokus erstes Feld', + 'es-es': 'Enfoque el primer campo', + 'fr-fr': 'Concentrez-vous sur le premier champ', + 'ru-ru': 'Фокус первого поля', + 'uk-ua': 'Перейти до першого поля', + 'pt-br': 'Foco primeiro no campo', }, doubleClickZoom: { - "en-us": "Double click to zoom", - "ru-ru": "Дважды щелкните, чтобы увеличить", - "es-es": "Haga doble clic para ampliar", - "fr-fr": "Double-cliquez pour zoomer", - "uk-ua": "Двічі клацніть, щоб збільшити", - "de-ch": "Zum Vergrößern doppelklicken", - "pt-br": "Clique duas vezes para ampliar", + 'en-us': 'Double click to zoom', + 'ru-ru': 'Дважды щелкните, чтобы увеличить', + 'es-es': 'Haga doble clic para ampliar', + 'fr-fr': 'Double-cliquez pour zoomer', + 'uk-ua': 'Двічі клацніть, щоб збільшити', + 'de-ch': 'Zum Vergrößern doppelklicken', + 'pt-br': 'Clique duas vezes para ampliar', }, closePopupOnClick: { - "en-us": "Close pop-up on outside click", - "ru-ru": "Закрытие всплывающего окна при внешнем щелчке", - "es-es": "Cerrar ventana emergente al hacer clic desde fuera", - "fr-fr": "Fermer la pop-up lors d'un clic extérieur", - "uk-ua": "Закрити спливаюче вікно при зовнішньому клацанні", - "de-ch": "Popup bei externem Klick schließen", - "pt-br": "Fechar pop-up ao clicar fora", + 'en-us': 'Close pop-up on outside click', + 'ru-ru': 'Закрытие всплывающего окна при внешнем щелчке', + 'es-es': 'Cerrar ventana emergente al hacer clic desde fuera', + 'fr-fr': "Fermer la pop-up lors d'un clic extérieur", + 'uk-ua': 'Закрити спливаюче вікно при зовнішньому клацанні', + 'de-ch': 'Popup bei externem Klick schließen', + 'pt-br': 'Fechar pop-up ao clicar fora', }, animateTransitions: { - "en-us": "Animate transitions", - "ru-ru": "Анимированные переходы", - "es-es": "Transiciones animadas", - "fr-fr": "Animer les transitions", - "uk-ua": "Анімація переходів", - "de-ch": "Übergänge animieren", - "pt-br": "Transições animadas", + 'en-us': 'Animate transitions', + 'ru-ru': 'Анимированные переходы', + 'es-es': 'Transiciones animadas', + 'fr-fr': 'Animer les transitions', + 'uk-ua': 'Анімація переходів', + 'de-ch': 'Übergänge animieren', + 'pt-br': 'Transições animadas', }, panInertia: { - "en-us": "Pan inertia", - "ru-ru": "Инерция пан", - "es-es": "Inercia de la sartén", - "fr-fr": "Inertie du bac", - "uk-ua": "Інерція панорами", - "de-ch": "Schwenkträgheit", - "pt-br": "Inércia da panela", + 'en-us': 'Pan inertia', + 'ru-ru': 'Инерция пан', + 'es-es': 'Inercia de la sartén', + 'fr-fr': 'Inertie du bac', + 'uk-ua': 'Інерція панорами', + 'de-ch': 'Schwenkträgheit', + 'pt-br': 'Inércia da panela', }, mouseDrags: { - "en-us": "Mouse drags", - "ru-ru": "Перетаскивание мышью", - "es-es": "El ratón arrastra", - "uk-ua": "Виділіть відповідний підрядок", - "de-ch": "Maus zieht", - "fr-fr": "Mettre en surbrillance la sous-chaîne correspondante", - "pt-br": "Arrastos do mouse", + 'en-us': 'Mouse drags', + 'ru-ru': 'Перетаскивание мышью', + 'es-es': 'El ratón arrastra', + 'uk-ua': 'Виділіть відповідний підрядок', + 'de-ch': 'Maus zieht', + 'fr-fr': 'Mettre en surbrillance la sous-chaîne correspondante', + 'pt-br': 'Arrastos do mouse', }, scrollWheelZoom: { - "en-us": "Scroll wheel zoom", - "ru-ru": "Масштабирование с помощью колеса прокрутки", - "es-es": "Zoom con rueda de desplazamiento", - "fr-fr": "Zoom avec la molette de défilement", - "uk-ua": "Масштаб колеса прокрутки", - "de-ch": "Scrollrad-Zoom", - "pt-br": "Zoom da roda de rolagem", + 'en-us': 'Scroll wheel zoom', + 'ru-ru': 'Масштабирование с помощью колеса прокрутки', + 'es-es': 'Zoom con rueda de desplazamiento', + 'fr-fr': 'Zoom avec la molette de défilement', + 'uk-ua': 'Масштаб колеса прокрутки', + 'de-ch': 'Scrollrad-Zoom', + 'pt-br': 'Zoom da roda de rolagem', }, flexibleColumnWidth: { - "en-us": "Flexible column width", - "ru-ru": "Гибкая ширина столбца", - "es-es": "Ancho de columna flexible", - "fr-fr": "Largeur de colonne flexible", - "uk-ua": "Гнучка ширина колонки", - "de-ch": "Flexible Spaltenbreite", - "pt-br": "Largura de coluna flexível", + 'en-us': 'Flexible column width', + 'ru-ru': 'Гибкая ширина столбца', + 'es-es': 'Ancho de columna flexible', + 'fr-fr': 'Largeur de colonne flexible', + 'uk-ua': 'Гнучка ширина колонки', + 'de-ch': 'Flexible Spaltenbreite', + 'pt-br': 'Largura de coluna flexível', }, flexibleSubGridColumnWidth: { - "en-us": "Flexible subview grid column width", - "ru-ru": "Гибкая ширина столбца сетки подпредставлений", - "es-es": "Ancho de columna de cuadrícula de subvista flexible", - "fr-fr": "Largeur de colonne de grille de sous-vue flexible", - "uk-ua": "Гнучка ширина стовпця сітки вкладеного перегляду", - "de-ch": "Flexible Spaltenbreite des Unteransichtsrasters", - "pt-br": "Largura flexível da coluna da grade de subvisualização", + 'en-us': 'Flexible subview grid column width', + 'ru-ru': 'Гибкая ширина столбца сетки подпредставлений', + 'es-es': 'Ancho de columna de cuadrícula de subvista flexible', + 'fr-fr': 'Largeur de colonne de grille de sous-vue flexible', + 'uk-ua': 'Гнучка ширина стовпця сітки вкладеного перегляду', + 'de-ch': 'Flexible Spaltenbreite des Unteransichtsrasters', + 'pt-br': 'Largura flexível da coluna da grade de subvisualização', }, closeOnEsc: { - "en-us": "Close on ESC key press", - "ru-ru": "Закрыть нажатием клавиши ESC", - "es-es": "Cerrar al presionar la tecla ESC", - "fr-fr": "Icône et nom de la table", - "uk-ua": "Закриття натисканням клавіші ESC", - "de-ch": "Schließen durch Drücken der Taste ESC", - "pt-br": "Fechar ao pressionar a tecla ESC", + 'en-us': 'Close on ESC key press', + 'ru-ru': 'Закрыть нажатием клавиши ESC', + 'es-es': 'Cerrar al presionar la tecla ESC', + 'fr-fr': 'Icône et nom de la table', + 'uk-ua': 'Закриття натисканням клавіші ESC', + 'de-ch': 'Schließen durch Drücken der Taste ESC', + 'pt-br': 'Fechar ao pressionar a tecla ESC', }, closeOnOutsideClick: { - "en-us": "Close on outside click", - "ru-ru": "Закрытие по внешнему щелчку", - "es-es": "Cerrar al hacer clic desde fuera", - "fr-fr": "Fermer sur clic extérieur", - "uk-ua": "Закрийте зовнішнім клацанням", - "de-ch": "Schließen durch Klicken von außen", - "pt-br": "Fechar com clique externo", + 'en-us': 'Close on outside click', + 'ru-ru': 'Закрытие по внешнему щелчку', + 'es-es': 'Cerrar al hacer clic desde fuera', + 'fr-fr': 'Fermer sur clic extérieur', + 'uk-ua': 'Закрийте зовнішнім клацанням', + 'de-ch': 'Schließen durch Klicken von außen', + 'pt-br': 'Fechar com clique externo', }, specifyNetworkBadge: { - "en-us": "Specify Network Badge", - "ru-ru": "Укажите сетевой значок", - "es-es": "Especificar la insignia de red", - "fr-fr": "Spécifier le badge réseau", - "uk-ua": "Укажіть значок мережі", - "de-ch": "Netzwerk-Badge angeben", - "pt-br": "Especificar emblema de rede", + 'en-us': 'Specify Network Badge', + 'ru-ru': 'Укажите сетевой значок', + 'es-es': 'Especificar la insignia de red', + 'fr-fr': 'Spécifier le badge réseau', + 'uk-ua': 'Укажіть значок мережі', + 'de-ch': 'Netzwerk-Badge angeben', + 'pt-br': 'Especificar emblema de rede', }, useAccessibleFullDatePicker: { - "en-us": "Use accessible full date picker", - "ru-ru": "Используйте доступный полный выбор даты", - "es-es": "Utilice el selector de fecha completo y accesible", - "fr-fr": "Utiliser un sélecteur de date complet accessible", - "uk-ua": "Використовуйте доступний повний засіб вибору дати", - "de-ch": "Verwenden Sie eine barrierefreie Datumsauswahl", - "pt-br": "Use o seletor de data completo acessível", + 'en-us': 'Use accessible full date picker', + 'ru-ru': 'Используйте доступный полный выбор даты', + 'es-es': 'Utilice el selector de fecha completo y accesible', + 'fr-fr': 'Utiliser un sélecteur de date complet accessible', + 'uk-ua': 'Використовуйте доступний повний засіб вибору дати', + 'de-ch': 'Verwenden Sie eine barrierefreie Datumsauswahl', + 'pt-br': 'Use o seletor de data completo acessível', }, useAccessibleMonthPicker: { - "en-us": "Use accessible month picker", - "ru-ru": "Используйте доступный выбор месяца", - "es-es": "Utilice el selector de meses accesible", - "fr-fr": "Utiliser le sélecteur de mois accessible", - "uk-ua": "Використовуйте доступний засіб вибору місяця", - "de-ch": "Verwenden Sie die barrierefreie Monatsauswahl", - "pt-br": "Use o seletor de meses acessível", + 'en-us': 'Use accessible month picker', + 'ru-ru': 'Используйте доступный выбор месяца', + 'es-es': 'Utilice el selector de meses accesible', + 'fr-fr': 'Utiliser le sélecteur de mois accessible', + 'uk-ua': 'Використовуйте доступний засіб вибору місяця', + 'de-ch': 'Verwenden Sie die barrierefreie Monatsauswahl', + 'pt-br': 'Use o seletor de meses acessível', }, rightAlignNumberFields: { - "en-us": "Right-Justify numeric fields", - "ru-ru": "Выравнивание числовых полей по правому краю", - "es-es": "Justificar a la derecha los campos numéricos", - "fr-fr": "Justifier à droite les champs numériques", - "uk-ua": "Вирівнювання по правому краю числових полів", - "de-ch": "Rechtsbündige Ausrichtung numerischer Felder", - "pt-br": "Justificar à direita campos numéricos", + 'en-us': 'Right-Justify numeric fields', + 'ru-ru': 'Выравнивание числовых полей по правому краю', + 'es-es': 'Justificar a la derecha los campos numéricos', + 'fr-fr': 'Justifier à droite les champs numériques', + 'uk-ua': 'Вирівнювання по правому краю числових полів', + 'de-ch': 'Rechtsbündige Ausrichtung numerischer Felder', + 'pt-br': 'Justificar à direita campos numéricos', }, roundedCorners: { - "en-us": "Rounded corners", - "ru-ru": "Закругленные углы", - "es-es": "esquinas redondeadas", - "fr-fr": "Coins arrondis", - "uk-ua": "Заокруглені кути", - "de-ch": "Abgerundete Ecken", - "pt-br": "Cantos arredondados", + 'en-us': 'Rounded corners', + 'ru-ru': 'Закругленные углы', + 'es-es': 'esquinas redondeadas', + 'fr-fr': 'Coins arrondis', + 'uk-ua': 'Заокруглені кути', + 'de-ch': 'Abgerundete Ecken', + 'pt-br': 'Cantos arredondados', }, showSubviewBorders: { - "en-us": "Show borders around subviews", - "de-ch": "Rahmen um Unteransichten anzeigen", - "es-es": "Mostrar bordes alrededor de las subvistas", - "fr-fr": "Afficher les bordures autour des sous-vues", - "pt-br": "Mostrar bordas ao redor das subvisualizações", - "ru-ru": "Показывать границы вокруг подпредставлений", - "uk-ua": "Показати межі навколо підвидів", + 'en-us': 'Show borders around subviews', + 'de-ch': 'Rahmen um Unteransichten anzeigen', + 'es-es': 'Mostrar bordes alrededor de las subvistas', + 'fr-fr': 'Afficher les bordures autour des sous-vues', + 'pt-br': 'Mostrar bordas ao redor das subvisualizações', + 'ru-ru': 'Показывать границы вокруг подпредставлений', + 'uk-ua': 'Показати межі навколо підвидів', }, limitMaxFieldWidth: { - "en-us": "Limit max field width", - "ru-ru": "Ограничить максимальную ширину поля", - "es-es": "Limitar el ancho máximo del campo", - "fr-fr": "Limiter la largeur maximale du champ", - "uk-ua": "Обмеження максимальної ширини поля", - "de-ch": "Maximale Feldbreite begrenzen", - "pt-br": "Limite máximo de largura do campo", + 'en-us': 'Limit max field width', + 'ru-ru': 'Ограничить максимальную ширину поля', + 'es-es': 'Limitar el ancho máximo del campo', + 'fr-fr': 'Limiter la largeur maximale du champ', + 'uk-ua': 'Обмеження максимальної ширини поля', + 'de-ch': 'Maximale Feldbreite begrenzen', + 'pt-br': 'Limite máximo de largura do campo', }, condenseQueryResults: { - "en-us": "Condense query results", - "ru-ru": "Сжать результаты запроса", - "es-es": "Condensar los resultados de la consulta", - "fr-fr": "Condenser les résultats de la requête", - "uk-ua": "Згорнути результати запиту", - "de-ch": "Abfrageergebnisse verdichten", - "pt-br": "Condensar resultados da consulta", + 'en-us': 'Condense query results', + 'ru-ru': 'Сжать результаты запроса', + 'es-es': 'Condensar los resultados de la consulta', + 'fr-fr': 'Condenser les résultats de la requête', + 'uk-ua': 'Згорнути результати запиту', + 'de-ch': 'Abfrageergebnisse verdichten', + 'pt-br': 'Condensar resultados da consulta', }, blurContentBehindDialog: { - "en-us": "Blur content behind the dialog", - "ru-ru": "Размытие содержимого за диалогом", - "es-es": "Desenfocar el contenido detrás del diálogo", - "fr-fr": "Flou le contenu derrière la boîte de dialogue", - "uk-ua": "Розмити вміст за діалоговим вікном", - "de-ch": "Inhalte hinter dem Dialog verwischen", - "pt-br": "Desfocar o conteúdo atrás do diálogo", + 'en-us': 'Blur content behind the dialog', + 'ru-ru': 'Размытие содержимого за диалогом', + 'es-es': 'Desenfocar el contenido detrás del diálogo', + 'fr-fr': 'Flou le contenu derrière la boîte de dialogue', + 'uk-ua': 'Розмити вміст за діалоговим вікном', + 'de-ch': 'Inhalte hinter dem Dialog verwischen', + 'pt-br': 'Desfocar o conteúdo atrás do diálogo', }, collectionSortOrderDescription: { - "en-us": "This determines the visual order of collections.", - "ru-ru": "Это определяет визуальный порядок коллекций.", - "es-es": "Esto determina el orden visual de las colecciones.", - "fr-fr": "Ceci détermine l'ordre visuel des collections.", - "uk-ua": "Це визначає візуальний порядок колекцій.", - "de-ch": "Dies bestimmt die visuelle Reihenfolge der Sammlungen.", - "pt-br": "Isso determina a ordem visual das coleções.", + 'en-us': 'This determines the visual order of collections.', + 'ru-ru': 'Это определяет визуальный порядок коллекций.', + 'es-es': 'Esto determina el orden visual de las colecciones.', + 'fr-fr': "Ceci détermine l'ordre visuel des collections.", + 'uk-ua': 'Це визначає візуальний порядок колекцій.', + 'de-ch': 'Dies bestimmt die visuelle Reihenfolge der Sammlungen.', + 'pt-br': 'Isso determina a ordem visual das coleções.', }, recordSetRecordToOpen: { - "en-us": "Record to open by default", - "ru-ru": "Запись для открытия по умолчанию", - "es-es": "Registro para abrir por defecto", - "fr-fr": "Enregistrement à ouvrir par défaut", - "uk-ua": "Запис відкривається за умовчанням", - "de-ch": "Standardmäßig zu öffnender Datensatz", - "pt-br": "Gravar para abrir por padrão", + 'en-us': 'Record to open by default', + 'ru-ru': 'Запись для открытия по умолчанию', + 'es-es': 'Registro para abrir por defecto', + 'fr-fr': 'Enregistrement à ouvrir par défaut', + 'uk-ua': 'Запис відкривається за умовчанням', + 'de-ch': 'Standardmäßig zu öffnender Datensatz', + 'pt-br': 'Gravar para abrir por padrão', }, altClickToSupressNewTab: { - "en-us": - "{altKeyName:string}+Click to suppress new tab", - "ru-ru": - "{altKeyName:string}+Нажмите , чтобы скрыть новую вкладку", - "es-es": - "{altKeyName:string}+Haga clic en para suprimir la nueva pestaña", - "fr-fr": - "{altKeyName:string}+Cliquez sur pour supprimer le nouvel onglet", - "uk-ua": - "{altKeyName:string}+Натисніть , щоб закрити нову вкладку", - "de-ch": - "{altKeyName:string}+Klicken Sie auf, um neue Registerkarten zu unterdrücken", - "pt-br": - "{altKeyName:string}+Clique em para suprimir a nova guia", + 'en-us': + '{altKeyName:string}+Click to suppress new tab', + 'ru-ru': + '{altKeyName:string}+Нажмите , чтобы скрыть новую вкладку', + 'es-es': + '{altKeyName:string}+Haga clic en para suprimir la nueva pestaña', + 'fr-fr': + '{altKeyName:string}+Cliquez sur pour supprimer le nouvel onglet', + 'uk-ua': + '{altKeyName:string}+Натисніть , щоб закрити нову вкладку', + 'de-ch': + '{altKeyName:string}+Klicken Sie auf, um neue Registerkarten zu unterdrücken', + 'pt-br': + '{altKeyName:string}+Clique em para suprimir a nova guia', }, altClickToSupressNewTabDescription: { - "en-us": - "{altKeyName:string}+Click a link that usually opens in a new tab to open it in the current tab.", - "ru-ru": - "{altKeyName:string}+Нажмите на ссылку, которая обычно открывается в новой вкладке, чтобы открыть ее в текущей вкладке.", - "es-es": - "{altKeyName:string}+Haga clic en un enlace que normalmente se abre en una nueva pestaña para abrirlo en la pestaña actual.", - "fr-fr": "Utiliser le sélecteur de mois accessible.", - "uk-ua": - "{altKeyName:string}+Натисніть посилання, яке зазвичай відкривається в новій вкладці, щоб відкрити його в поточній вкладці.", - "de-ch": - "{altKeyName:string}+Klicken Sie auf einen Link, der normalerweise in einem neuen Tab geöffnet wird, um ihn im aktuellen Tab zu öffnen.", - "pt-br": - "{altKeyName:string}+Clique em um link que geralmente abre em uma nova aba para abri-lo na aba atual.", + 'en-us': + '{altKeyName:string}+Click a link that usually opens in a new tab to open it in the current tab.', + 'ru-ru': + '{altKeyName:string}+Нажмите на ссылку, которая обычно открывается в новой вкладке, чтобы открыть ее в текущей вкладке.', + 'es-es': + '{altKeyName:string}+Haga clic en un enlace que normalmente se abre en una nueva pestaña para abrirlo en la pestaña actual.', + 'fr-fr': 'Utiliser le sélecteur de mois accessible.', + 'uk-ua': + '{altKeyName:string}+Натисніть посилання, яке зазвичай відкривається в новій вкладці, щоб відкрити його в поточній вкладці.', + 'de-ch': + '{altKeyName:string}+Klicken Sie auf einen Link, der normalerweise in einem neuen Tab geöffnet wird, um ihn im aktuellen Tab zu öffnen.', + 'pt-br': + '{altKeyName:string}+Clique em um link que geralmente abre em uma nova aba para abri-lo na aba atual.', }, makeFormDialogsModal: { - "en-us": "Make form dialogs gray out the background", - "ru-ru": "Сделать фон диалоговых окон серым", - "es-es": - "Hacer que los cuadros de diálogo del formulario tengan el fondo en gris", - "fr-fr": + 'en-us': 'Make form dialogs gray out the background', + 'ru-ru': 'Сделать фон диалоговых окон серым', + 'es-es': + 'Hacer que los cuadros de diálogo del formulario tengan el fondo en gris', + 'fr-fr': "Rendre les boîtes de dialogue de formulaire grisées sur l'arrière-plan", - "uk-ua": "Зробіть діалогові вікна форми сірими фоном", - "de-ch": "Den Hintergrund von Formulardialogen ausgrauen", - "pt-br": - "Faça com que as caixas de diálogo do formulário fiquem com o fundo acinzentado", + 'uk-ua': 'Зробіть діалогові вікна форми сірими фоном', + 'de-ch': 'Den Hintergrund von Formulardialogen ausgrauen', + 'pt-br': + 'Faça com que as caixas de diálogo do formulário fiquem com o fundo acinzentado', }, autoScrollTree: { - "en-us": "Auto scroll tree to focused node", - "ru-ru": "Автоматическая прокрутка дерева к выбранному узлу", - "es-es": "Desplazamiento automático del árbol al nodo enfocado", - "fr-fr": "Arbre de défilement automatique vers le nœud ciblé", - "uk-ua": "Автоматичне прокручування дерева до виділеного вузла", - "de-ch": "Automatisches Scrollen des Baums zum fokussierten Knoten", - "pt-br": "Rolagem automática da árvore para o nó em foco", + 'en-us': 'Auto scroll tree to focused node', + 'ru-ru': 'Автоматическая прокрутка дерева к выбранному узлу', + 'es-es': 'Desplazamiento automático del árbol al nodo enfocado', + 'fr-fr': 'Arbre de défilement automatique vers le nœud ciblé', + 'uk-ua': 'Автоматичне прокручування дерева до виділеного вузла', + 'de-ch': 'Automatisches Scrollen des Baums zum fokussierten Knoten', + 'pt-br': 'Rolagem automática da árvore para o nó em foco', }, sortByField: { - "en-us": "Order By Field", - "de-ch": "Nach Feld sortieren", - "es-es": "Ordenar por campo", - "fr-fr": "Trier par champ", - "pt-br": "Ordenar por campo", - "ru-ru": "Сортировать по полю", - "uk-ua": "Сортувати за полем", + 'en-us': 'Order By Field', + 'de-ch': 'Nach Feld sortieren', + 'es-es': 'Ordenar por campo', + 'fr-fr': 'Trier par champ', + 'pt-br': 'Ordenar por campo', + 'ru-ru': 'Сортировать по полю', + 'uk-ua': 'Сортувати за полем', }, lineWrap: { - "en-us": "Line wrap", - "ru-ru": "Перенос строки", - "es-es": "Ajuste de línea", - "fr-fr": "Retour à la ligne", - "uk-ua": "Обтікання лініями", - "de-ch": "Zeilenumbruch", - "pt-br": "Quebra de linha", + 'en-us': 'Line wrap', + 'ru-ru': 'Перенос строки', + 'es-es': 'Ajuste de línea', + 'fr-fr': 'Retour à la ligne', + 'uk-ua': 'Обтікання лініями', + 'de-ch': 'Zeilenumbruch', + 'pt-br': 'Quebra de linha', }, indentSize: { - "en-us": "Indent size", - "ru-ru": "Размер отступа", - "es-es": "Tamaño de sangría", - "fr-fr": "Taille du retrait", - "uk-ua": "Розмір відступу", - "de-ch": "Einzugsgröße", - "pt-br": "Tamanho do recuo", + 'en-us': 'Indent size', + 'ru-ru': 'Размер отступа', + 'es-es': 'Tamaño de sangría', + 'fr-fr': 'Taille du retrait', + 'uk-ua': 'Розмір відступу', + 'de-ch': 'Einzugsgröße', + 'pt-br': 'Tamanho do recuo', }, indentWithTab: { - "en-us": "Indent with Tab", - "ru-ru": "Отступ с помощью Tab", - "es-es": "Sangría con Tab", - "fr-fr": "Indenter avec Tabulation", - "uk-ua": "Відступ із Tab", - "de-ch": "Einrücken mit Tab", - "pt-br": "Recuo com Tab", + 'en-us': 'Indent with Tab', + 'ru-ru': 'Отступ с помощью Tab', + 'es-es': 'Sangría con Tab', + 'fr-fr': 'Indenter avec Tabulation', + 'uk-ua': 'Відступ із Tab', + 'de-ch': 'Einrücken mit Tab', + 'pt-br': 'Recuo com Tab', }, formHeaderFormat: { - "en-us": "Form header format", - "ru-ru": "Формат заголовка формы", - "es-es": "Formato del encabezado del formulario", - "fr-fr": "Format d'en-tête de formulaire", - "uk-ua": "Формат заголовка форми", - "de-ch": "Formularkopfformat", - "pt-br": "Formato do cabeçalho do formulário", + 'en-us': 'Form header format', + 'ru-ru': 'Формат заголовка формы', + 'es-es': 'Formato del encabezado del formulario', + 'fr-fr': "Format d'en-tête de formulaire", + 'uk-ua': 'Формат заголовка форми', + 'de-ch': 'Formularkopfformat', + 'pt-br': 'Formato do cabeçalho do formulário', }, iconAndTableName: { - "en-us": "Icon and table name", - "ru-ru": "Значок и название таблицы", - "es-es": "Icono y nombre de la tabla", - "fr-fr": "Icône et nom de la table", - "uk-ua": "Значок і назва таблиці", - "de-ch": "Symbol und Tabellenname", - "pt-br": "Ícone e nome da tabela", + 'en-us': 'Icon and table name', + 'ru-ru': 'Значок и название таблицы', + 'es-es': 'Icono y nombre de la tabla', + 'fr-fr': 'Icône et nom de la table', + 'uk-ua': 'Значок і назва таблиці', + 'de-ch': 'Symbol und Tabellenname', + 'pt-br': 'Ícone e nome da tabela', }, tableIcon: { - "en-us": "Table icon", - "ru-ru": "Значок таблицы", - "es-es": "Icono de tabla", - "fr-fr": "Icône de tableau", - "uk-ua": "Значок таблиці", - "de-ch": "Tabellensymbol", - "pt-br": "Ícone de tabela", + 'en-us': 'Table icon', + 'ru-ru': 'Значок таблицы', + 'es-es': 'Icono de tabla', + 'fr-fr': 'Icône de tableau', + 'uk-ua': 'Значок таблиці', + 'de-ch': 'Tabellensymbol', + 'pt-br': 'Ícone de tabela', }, maxHeight: { - "en-us": "Max height", - "ru-ru": "Максимальная высота", - "es-es": "Altura máxima", - "fr-fr": "hauteur maximum", - "uk-ua": "Максимальна висота", - "de-ch": "Maximale Höhe", - "pt-br": "Altura máxima", + 'en-us': 'Max height', + 'ru-ru': 'Максимальная высота', + 'es-es': 'Altura máxima', + 'fr-fr': 'hauteur maximum', + 'uk-ua': 'Максимальна висота', + 'de-ch': 'Maximale Höhe', + 'pt-br': 'Altura máxima', }, autoComplete: { - "en-us": "Auto complete", - "ru-ru": "Автозаполнение", - "es-es": "Autocompletar", - "fr-fr": + 'en-us': 'Auto complete', + 'ru-ru': 'Автозаполнение', + 'es-es': 'Autocompletar', + 'fr-fr': "Détermine les légendes des champs, les notes d'utilisation et les légendes des tableaux", - "uk-ua": - "Визначає підписи полів, примітки щодо використання та підписи таблиць", - "de-ch": "Autovervollständigung", - "pt-br": "Preenchimento automático", + 'uk-ua': + 'Визначає підписи полів, примітки щодо використання та підписи таблиць', + 'de-ch': 'Autovervollständigung', + 'pt-br': 'Preenchimento automático', }, searchCaseSensitive: { - "en-us": "Case-sensitive search", - "es-es": "Búsqueda que distingue entre mayúsculas y minúsculas", - "fr-fr": "Recherche sensible à la casse", - "uk-ua": "Пошук з урахуванням регістру", - "de-ch": "Groß- und Kleinschreibung beachten", - "ru-ru": "Поиск с учетом регистра", - "pt-br": "Pesquisa com diferenciação entre maiúsculas e minúsculas", + 'en-us': 'Case-sensitive search', + 'es-es': 'Búsqueda que distingue entre mayúsculas y minúsculas', + 'fr-fr': 'Recherche sensible à la casse', + 'uk-ua': 'Пошук з урахуванням регістру', + 'de-ch': 'Groß- und Kleinschreibung beachten', + 'ru-ru': 'Поиск с учетом регистра', + 'pt-br': 'Pesquisa com diferenciação entre maiúsculas e minúsculas', }, searchField: { - "en-us": "Search Field", - "ru-ru": "Поле поиска", - "es-es": "Campo de búsqueda", - "fr-fr": "Champ de recherche", - "uk-ua": "Поле пошуку", - "de-ch": "Suchfeld", - "pt-br": "Campo de pesquisa", + 'en-us': 'Search Field', + 'ru-ru': 'Поле поиска', + 'es-es': 'Campo de búsqueda', + 'fr-fr': 'Champ de recherche', + 'uk-ua': 'Поле пошуку', + 'de-ch': 'Suchfeld', + 'pt-br': 'Campo de pesquisa', }, createInteractions: { - "en-us": "Creating an interaction", - "ru-ru": "Создание взаимодействия", - "es-es": "Creando una interacción", - "fr-fr": "Créer une interaction", - "uk-ua": "Створення взаємодії", - "de-ch": "Erstellen einer Interaktion", - "pt-br": "Criando uma interação", + 'en-us': 'Creating an interaction', + 'ru-ru': 'Создание взаимодействия', + 'es-es': 'Creando una interacción', + 'fr-fr': 'Créer une interaction', + 'uk-ua': 'Створення взаємодії', + 'de-ch': 'Erstellen einer Interaktion', + 'pt-br': 'Criando uma interação', }, useSpaceAsDelimiter: { - "en-us": "Use space as delimiter", - "ru-ru": "Используйте пробел в качестве разделителя", - "es-es": "Utilice el espacio como delimitador", - "fr-fr": "Utiliser l'espace comme délimiteur", - "uk-ua": "Використовуйте пробіл як роздільник", - "de-ch": "Leerzeichen als Trennzeichen verwenden", - "pt-br": "Use espaço como delimitador", + 'en-us': 'Use space as delimiter', + 'ru-ru': 'Используйте пробел в качестве разделителя', + 'es-es': 'Utilice el espacio como delimitador', + 'fr-fr': "Utiliser l'espace comme délimiteur", + 'uk-ua': 'Використовуйте пробіл як роздільник', + 'de-ch': 'Leerzeichen als Trennzeichen verwenden', + 'pt-br': 'Use espaço como delimitador', }, useCommaAsDelimiter: { - "en-us": "Use comma as delimiter", - "ru-ru": "Используйте запятую в качестве разделителя", - "es-es": "Utilice la coma como delimitador", - "fr-fr": "Utiliser la virgule comme délimiteur", - "uk-ua": "Використовуйте кому як роздільник", - "de-ch": "Verwenden Sie Kommas als Trennzeichen.", - "pt-br": "Use vírgula como delimitador", + 'en-us': 'Use comma as delimiter', + 'ru-ru': 'Используйте запятую в качестве разделителя', + 'es-es': 'Utilice la coma como delimitador', + 'fr-fr': 'Utiliser la virgule comme délimiteur', + 'uk-ua': 'Використовуйте кому як роздільник', + 'de-ch': 'Verwenden Sie Kommas als Trennzeichen.', + 'pt-br': 'Use vírgula como delimitador', }, useNewLineAsDelimiter: { - "en-us": "Use new line as delimiter", - "ru-ru": "Использовать новую строку в качестве разделителя", - "es-es": "Utilice nueva línea como delimitador", - "fr-fr": "Utiliser une nouvelle ligne comme délimiteur", - "uk-ua": "Використовуйте новий рядок як роздільник", - "de-ch": "Neue Zeile als Trennzeichen verwenden", - "pt-br": "Use nova linha como delimitador", + 'en-us': 'Use new line as delimiter', + 'ru-ru': 'Использовать новую строку в качестве разделителя', + 'es-es': 'Utilice nueva línea como delimitador', + 'fr-fr': 'Utiliser une nouvelle ligne comme délimiteur', + 'uk-ua': 'Використовуйте новий рядок як роздільник', + 'de-ch': 'Neue Zeile als Trennzeichen verwenden', + 'pt-br': 'Use nova linha como delimitador', }, useCustomDelimiters: { - "en-us": "Use custom delimiters", - "ru-ru": "Используйте пользовательские разделители", - "es-es": "Utilice delimitadores personalizados", - "fr-fr": "Utiliser des délimiteurs personnalisés", - "uk-ua": "Використовуйте спеціальні роздільники", - "de-ch": "Benutzerdefinierte Trennzeichen verwenden", - "pt-br": "Use delimitadores personalizados", + 'en-us': 'Use custom delimiters', + 'ru-ru': 'Используйте пользовательские разделители', + 'es-es': 'Utilice delimitadores personalizados', + 'fr-fr': 'Utiliser des délimiteurs personnalisés', + 'uk-ua': 'Використовуйте спеціальні роздільники', + 'de-ch': 'Benutzerdefinierte Trennzeichen verwenden', + 'pt-br': 'Use delimitadores personalizados', }, useCustomDelimitersDescription: { - "en-us": - "A list of delimiters to use, in addition to the ones defined above. Put one delimiter per line.", - "ru-ru": - "Список разделителей, которые можно использовать в дополнение к указанным выше. Используйте по одному разделителю на строку.", - "es-es": - "Una lista de delimitadores para usar, además de los definidos anteriormente. Coloque un delimitador por línea.", - "fr-fr": - "Une liste de délimiteurs à utiliser, en plus de ceux définis ci-dessus. Mettez un délimiteur par ligne.", - "uk-ua": - "Список розділювачів для використання на додаток до визначених вище. Поставте один роздільник на рядок.", - "de-ch": - "Eine Liste der zu verwendenden Trennzeichen zusätzlich zu den oben definierten. Geben Sie pro Zeile ein Trennzeichen ein.", - "pt-br": - "Uma lista de delimitadores a serem usados, além dos definidos acima. Coloque um delimitador por linha.", + 'en-us': + 'A list of delimiters to use, in addition to the ones defined above. Put one delimiter per line.', + 'ru-ru': + 'Список разделителей, которые можно использовать в дополнение к указанным выше. Используйте по одному разделителю на строку.', + 'es-es': + 'Una lista de delimitadores para usar, además de los definidos anteriormente. Coloque un delimitador por línea.', + 'fr-fr': + 'Une liste de délimiteurs à utiliser, en plus de ceux définis ci-dessus. Mettez un délimiteur par ligne.', + 'uk-ua': + 'Список розділювачів для використання на додаток до визначених вище. Поставте один роздільник на рядок.', + 'de-ch': + 'Eine Liste der zu verwendenden Trennzeichen zusätzlich zu den oben definierten. Geben Sie pro Zeile ein Trennzeichen ein.', + 'pt-br': + 'Uma lista de delimitadores a serem usados, além dos definidos acima. Coloque um delimitador por linha.', }, detectAutomaticallyDescription: { - "en-us": "Detect automatically based on catalog number format.", - "ru-ru": "Автоматическое определение на основе формата каталожного номера.", - "es-es": - "Detectar automáticamente según el formato del número de catálogo.", - "fr-fr": - "Détecter automatiquement en fonction du format du numéro de catalogue.", - "uk-ua": "Визначати автоматично на основі формату номера каталогу.", - "de-ch": "Automatische Erkennung basierend auf dem Katalognummernformat.", - "pt-br": - "Detecte automaticamente com base no formato do número de catálogo.", + 'en-us': 'Detect automatically based on catalog number format.', + 'ru-ru': 'Автоматическое определение на основе формата каталожного номера.', + 'es-es': + 'Detectar automáticamente según el formato del número de catálogo.', + 'fr-fr': + 'Détecter automatiquement en fonction du format du numéro de catalogue.', + 'uk-ua': 'Визначати автоматично на основі формату номера каталогу.', + 'de-ch': 'Automatische Erkennung basierend auf dem Katalognummernformat.', + 'pt-br': + 'Detecte automaticamente com base no formato do número de catálogo.', }, use: { - comment: "Verb", - "en-us": "Use", - "ru-ru": "Использовать", - "es-es": "Usar", - "fr-fr": "Utiliser", - "uk-ua": "використання", - "de-ch": "Verwenden", - "pt-br": "Usar", + comment: 'Verb', + 'en-us': 'Use', + 'ru-ru': 'Использовать', + 'es-es': 'Usar', + 'fr-fr': 'Utiliser', + 'uk-ua': 'використання', + 'de-ch': 'Verwenden', + 'pt-br': 'Usar', }, dontUse: { - "en-us": "Don’t use", - "ru-ru": "Не использовать", - "es-es": "No utilizar", - "fr-fr": "Zoom avec la molette de défilement", - "uk-ua": "Масштаб колеса прокрутки", - "de-ch": "Nicht verwenden", - "pt-br": "Não use", + 'en-us': 'Don’t use', + 'ru-ru': 'Не использовать', + 'es-es': 'No utilizar', + 'fr-fr': 'Zoom avec la molette de défilement', + 'uk-ua': 'Масштаб колеса прокрутки', + 'de-ch': 'Nicht verwenden', + 'pt-br': 'Não use', }, position: { - "en-us": "Position", - "es-es": "Posición", - "fr-fr": "Position", - "ru-ru": "Позиция", - "uk-ua": "Позиція", - "de-ch": "Position", - "pt-br": "Posição", + 'en-us': 'Position', + 'es-es': 'Posición', + 'fr-fr': 'Position', + 'ru-ru': 'Позиция', + 'uk-ua': 'Позиція', + 'de-ch': 'Position', + 'pt-br': 'Posição', }, top: { - "en-us": "Top", - "es-es": "Arriba", - "fr-fr": "Haut", - "ru-ru": "Вершина", - "uk-ua": "Топ", - "de-ch": "Spitze", - "pt-br": "Principal", + 'en-us': 'Top', + 'es-es': 'Arriba', + 'fr-fr': 'Haut', + 'ru-ru': 'Вершина', + 'uk-ua': 'Топ', + 'de-ch': 'Spitze', + 'pt-br': 'Principal', }, bottom: { - "en-us": "Bottom", - "es-es": "Abajo", - "ru-ru": "Нижний", - "uk-ua": "Дно", - "de-ch": "Unten", - "fr-fr": "Bas", - "pt-br": "Fundo", + 'en-us': 'Bottom', + 'es-es': 'Abajo', + 'ru-ru': 'Нижний', + 'uk-ua': 'Дно', + 'de-ch': 'Unten', + 'fr-fr': 'Bas', + 'pt-br': 'Fundo', }, left: { - "en-us": "Left", - "es-es": "Izquierda", - "fr-fr": "Gauche", - "ru-ru": "Левый", - "uk-ua": "Ліворуч", - "de-ch": "Links", - "pt-br": "Esquerda", + 'en-us': 'Left', + 'es-es': 'Izquierda', + 'fr-fr': 'Gauche', + 'ru-ru': 'Левый', + 'uk-ua': 'Ліворуч', + 'de-ch': 'Links', + 'pt-br': 'Esquerda', }, right: { - "en-us": "Right", - "es-es": "Bien", - "fr-fr": "Droite", - "ru-ru": "Верно", - "uk-ua": "правильно", - "de-ch": "Rechts", - "pt-br": "Certo", + 'en-us': 'Right', + 'es-es': 'Bien', + 'fr-fr': 'Droite', + 'ru-ru': 'Верно', + 'uk-ua': 'правильно', + 'de-ch': 'Rechts', + 'pt-br': 'Certo', }, showUnsavedIndicator: { - "en-us": "Show unsaved changes indicator", - "ru-ru": "Показать индикатор несохраненных изменений", - "es-es": "Mostrar indicador de cambios no guardados", - "fr-fr": "Afficher l'indicateur de modifications non enregistrées", - "uk-ua": "Показати індикатор незбережених змін", - "de-ch": "Indikator für nicht gespeicherte Änderungen anzeigen", - "pt-br": "Mostrar indicador de alterações não salvas", + 'en-us': 'Show unsaved changes indicator', + 'ru-ru': 'Показать индикатор несохраненных изменений', + 'es-es': 'Mostrar indicador de cambios no guardados', + 'fr-fr': "Afficher l'indicateur de modifications non enregistrées", + 'uk-ua': 'Показати індикатор незбережених змін', + 'de-ch': 'Indikator für nicht gespeicherte Änderungen anzeigen', + 'pt-br': 'Mostrar indicador de alterações não salvas', }, showUnsavedIndicatorDescription: { - "en-us": + 'en-us': 'Show an "*" in the tab title when there are unsaved changes in the current tab.', - "es-es": + 'es-es': 'Mostrar un "*" en el título de la pestaña cuando haya cambios sin guardar en la pestaña actual.', - "fr-fr": + 'fr-fr': "Afficher un \"*\" dans le titre de l'onglet lorsqu'il y a des modifications non enregistrées dans l'onglet actuel.", - "ru-ru": - "Отображать «*» в заголовке вкладки, если на текущей вкладке есть несохраненные изменения.", - "uk-ua": - "Показувати «*» у заголовку вкладки, якщо в поточній вкладці є незбережені зміни.", - "de-ch": - "Zeigen Sie im Registerkartentitel ein „*“ an, wenn in der aktuellen Registerkarte nicht gespeicherte Änderungen vorhanden sind.", - "pt-br": + 'ru-ru': + 'Отображать «*» в заголовке вкладки, если на текущей вкладке есть несохраненные изменения.', + 'uk-ua': + 'Показувати «*» у заголовку вкладки, якщо в поточній вкладці є незбережені зміни.', + 'de-ch': + 'Zeigen Sie im Registerkartentitel ein „*“ an, wenn in der aktuellen Registerkarte nicht gespeicherte Änderungen vorhanden sind.', + 'pt-br': 'Exibir um "*" no título da aba quando houver alterações não salvas na aba atual.', }, autoPopulateDescription: { - "en-us": - "Auto populate the merged record with values from duplicates when opening the merging dialog.", - "ru-ru": - "Автоматически заполнять объединенную запись значениями из дубликатов при открытии диалогового окна слияния.", - "de-ch": - "Füllen Sie den zusammengeführten Datensatz beim Öffnen des Zusammenführungsdialogs automatisch mit Werten aus Duplikaten.", - "es-es": - "Rellene automáticamente el registro fusionado con valores de duplicados al abrir el cuadro de diálogo de fusión.", - "fr-fr": + 'en-us': + 'Auto populate the merged record with values from duplicates when opening the merging dialog.', + 'ru-ru': + 'Автоматически заполнять объединенную запись значениями из дубликатов при открытии диалогового окна слияния.', + 'de-ch': + 'Füllen Sie den zusammengeführten Datensatz beim Öffnen des Zusammenführungsdialogs automatisch mit Werten aus Duplikaten.', + 'es-es': + 'Rellene automáticamente el registro fusionado con valores de duplicados al abrir el cuadro de diálogo de fusión.', + 'fr-fr': "Remplir automatiquement l'enregistrement fusionné avec les valeurs des doublons lors de l'ouverture de la boîte de dialogue de fusion.", - "uk-ua": - "Автоматичне заповнення об’єднаного запису значеннями з дублікатів під час відкриття діалогового вікна об’єднання.", - "pt-br": - "Preencha automaticamente o registro mesclado com valores de duplicatas ao abrir a caixa de diálogo de mesclagem.", + 'uk-ua': + 'Автоматичне заповнення об’єднаного запису значеннями з дублікатів під час відкриття діалогового вікна об’єднання.', + 'pt-br': + 'Preencha automaticamente o registro mesclado com valores de duplicatas ao abrir a caixa de diálogo de mesclagem.', }, autoCreateVariants: { - "en-us": "Automatically create {agentVariantTable:string} records", - "ru-ru": "Автоматически создавать записи {agentVariantTable:string}", - "de-ch": "{agentVariantTable:string}-Datensätze automatisch erstellen", - "es-es": "Crear automáticamente registros {agentVariantTable:string}", - "fr-fr": - "Créer automatiquement des enregistrements {agentVariantTable:string}", - "uk-ua": "Автоматично створювати записи {agentVariantTable:string}", - "pt-br": "Criar automaticamente registros {agentVariantTable:string}", + 'en-us': 'Automatically create {agentVariantTable:string} records', + 'ru-ru': 'Автоматически создавать записи {agentVariantTable:string}', + 'de-ch': '{agentVariantTable:string}-Datensätze automatisch erstellen', + 'es-es': 'Crear automáticamente registros {agentVariantTable:string}', + 'fr-fr': + 'Créer automatiquement des enregistrements {agentVariantTable:string}', + 'uk-ua': 'Автоматично створювати записи {agentVariantTable:string}', + 'pt-br': 'Criar automaticamente registros {agentVariantTable:string}', }, autoCreateVariantsDescription: { - "en-us": - "When merging agents, automatically create {agentVariantTable:string} records based on the variations of first name/last name.", - "ru-ru": - "При объединении агентов автоматически создавать записи {agentVariantTable:string} на основе вариаций имени/фамилии.", - "de-ch": - "Beim Zusammenführen von Agenten werden automatisch {agentVariantTable:string}-Datensätze basierend auf den Variationen von Vorname/Nachname erstellt.", - "es-es": - "Al fusionar agentes, cree automáticamente registros {agentVariantTable:string} basados en las variaciones de nombre/apellido.", - "fr-fr": + 'en-us': + 'When merging agents, automatically create {agentVariantTable:string} records based on the variations of first name/last name.', + 'ru-ru': + 'При объединении агентов автоматически создавать записи {agentVariantTable:string} на основе вариаций имени/фамилии.', + 'de-ch': + 'Beim Zusammenführen von Agenten werden automatisch {agentVariantTable:string}-Datensätze basierend auf den Variationen von Vorname/Nachname erstellt.', + 'es-es': + 'Al fusionar agentes, cree automáticamente registros {agentVariantTable:string} basados en las variaciones de nombre/apellido.', + 'fr-fr': "Lors de la fusion d'agents, créez automatiquement des enregistrements {agentVariantTable:string} en fonction des variations du prénom/nom.", - "uk-ua": - "Під час об’єднання агентів автоматично створювати записи {agentVariantTable:string} на основі варіацій імені/прізвища.", - "pt-br": - "Ao mesclar agentes, crie automaticamente registros {agentVariantTable:string} com base nas variações de nome/sobrenome.", + 'uk-ua': + 'Під час об’єднання агентів автоматично створювати записи {agentVariantTable:string} на основі варіацій імені/прізвища.', + 'pt-br': + 'Ao mesclar agentes, crie automaticamente registros {agentVariantTable:string} com base nas variações de nome/sobrenome.', }, collectionPreferences: { - "en-us": "Collection Preferences", - "de-ch": "Sammlungseinstellungen", - "es-es": "Preferencias de colección", - "fr-fr": "Personnalisation", - "ru-ru": "Настройки коллекции", - "uk-ua": "Налаштування", - "pt-br": "Preferências de coleção", + 'en-us': 'Collection Preferences', + 'de-ch': 'Sammlungseinstellungen', + 'es-es': 'Preferencias de colección', + 'fr-fr': 'Personnalisation', + 'ru-ru': 'Настройки коллекции', + 'uk-ua': 'Налаштування', + 'pt-br': 'Preferências de coleção', }, rememberDialogSizes: { - "en-us": "Remember dialog window sizes", - "ru-ru": "Запомните размеры диалоговых окон", - "es-es": "Recordar los tamaños de las ventanas de diálogo", - "fr-fr": "Mémoriser les tailles des fenêtres de dialogue", - "uk-ua": "Запам'ятайте розміри діалогових вікон", - "de-ch": "Dialogfenstergrößen merken", - "pt-br": "Lembrar tamanhos de janelas de diálogo", + 'en-us': 'Remember dialog window sizes', + 'ru-ru': 'Запомните размеры диалоговых окон', + 'es-es': 'Recordar los tamaños de las ventanas de diálogo', + 'fr-fr': 'Mémoriser les tailles des fenêtres de dialogue', + 'uk-ua': "Запам'ятайте розміри діалогових вікон", + 'de-ch': 'Dialogfenstergrößen merken', + 'pt-br': 'Lembrar tamanhos de janelas de diálogo', }, rememberDialogPositions: { - "en-us": "Remember dialog window positions", - "ru-ru": "Запомнить позиции диалоговых окон", - "es-es": "Recordar las posiciones de las ventanas de diálogo", - "fr-fr": "Mémoriser les positions des fenêtres de dialogue", - "uk-ua": "Запам'ятовуйте положення діалогового вікна", - "de-ch": "Dialogfensterpositionen merken", - "pt-br": "Lembrar posições da janela de diálogo", + 'en-us': 'Remember dialog window positions', + 'ru-ru': 'Запомнить позиции диалоговых окон', + 'es-es': 'Recordar las posiciones de las ventanas de diálogo', + 'fr-fr': 'Mémoriser les positions des fenêtres de dialogue', + 'uk-ua': "Запам'ятовуйте положення діалогового вікна", + 'de-ch': 'Dialogfensterpositionen merken', + 'pt-br': 'Lembrar posições da janela de diálogo', }, autoPlayMedia: { - "en-us": "Automatically play media", - "ru-ru": "Автоматически воспроизводить медиа", - "es-es": "Reproducir automáticamente medios", - "fr-fr": "Lire automatiquement les médias", - "uk-ua": "Автоматичне відтворення медіа", - "de-ch": "Medien automatisch abspielen", - "pt-br": "Reproduzir mídia automaticamente", + 'en-us': 'Automatically play media', + 'ru-ru': 'Автоматически воспроизводить медиа', + 'es-es': 'Reproducir automáticamente medios', + 'fr-fr': 'Lire automatiquement les médias', + 'uk-ua': 'Автоматичне відтворення медіа', + 'de-ch': 'Medien automatisch abspielen', + 'pt-br': 'Reproduzir mídia automaticamente', }, useCustomTooltips: { - "en-us": "Use modern tooltips", - "ru-ru": "Используйте современные подсказки", - "es-es": "Utilice información sobre herramientas moderna", - "fr-fr": "Utiliser des info-bulles modernes", - "uk-ua": "Використовуйте сучасні підказки", - "de-ch": "Verwenden Sie moderne Tooltips", - "pt-br": "Use dicas de ferramentas modernas", + 'en-us': 'Use modern tooltips', + 'ru-ru': 'Используйте современные подсказки', + 'es-es': 'Utilice información sobre herramientas moderna', + 'fr-fr': 'Utiliser des info-bulles modernes', + 'uk-ua': 'Використовуйте сучасні підказки', + 'de-ch': 'Verwenden Sie moderne Tooltips', + 'pt-br': 'Use dicas de ferramentas modernas', }, alwaysUseQueryBuilder: { - "en-us": "Always use query builder search inside of search form", - "de-ch": - "Verwenden Sie innerhalb des Suchformulars immer die Abfragegeneratorsuche", - "es-es": - "Utilice siempre la búsqueda del generador de consultas dentro del formulario de búsqueda", - "fr-fr": - "Utilisez toujours la recherche du générateur de requêtes dans le formulaire de recherche", - "ru-ru": "Всегда используйте конструктор запросов внутри формы поиска.", - "uk-ua": "Завжди використовуйте пошук конструктора запитів у формі пошуку", - "pt-br": - "Sempre use a pesquisa do construtor de consultas dentro do formulário de pesquisa", + 'en-us': 'Always use query builder search inside of search form', + 'de-ch': + 'Verwenden Sie innerhalb des Suchformulars immer die Abfragegeneratorsuche', + 'es-es': + 'Utilice siempre la búsqueda del generador de consultas dentro del formulario de búsqueda', + 'fr-fr': + 'Utilisez toujours la recherche du générateur de requêtes dans le formulaire de recherche', + 'ru-ru': 'Всегда используйте конструктор запросов внутри формы поиска.', + 'uk-ua': 'Завжди використовуйте пошук конструктора запитів у формі пошуку', + 'pt-br': + 'Sempre use a pesquisa do construtor de consultas dentro do formulário de pesquisa', }, localizeResourceNames: { - "en-us": "Localize the names of recognized app resources", - "de-ch": "Lokalisieren Sie die Namen erkannter App-Ressourcen", - "es-es": - "Localizar los nombres de los recursos de aplicaciones reconocidos", - "fr-fr": "Localiser les noms des ressources d'application reconnues", - "ru-ru": "Локализуйте названия распознанных ресурсов приложения", - "uk-ua": "Локалізувати назви розпізнаних ресурсів програми", - "pt-br": "Localize os nomes dos recursos de aplicativos reconhecidos", + 'en-us': 'Localize the names of recognized app resources', + 'de-ch': 'Lokalisieren Sie die Namen erkannter App-Ressourcen', + 'es-es': + 'Localizar los nombres de los recursos de aplicaciones reconocidos', + 'fr-fr': "Localiser les noms des ressources d'application reconnues", + 'ru-ru': 'Локализуйте названия распознанных ресурсов приложения', + 'uk-ua': 'Локалізувати назви розпізнаних ресурсів програми', + 'pt-br': 'Localize os nomes dos recursos de aplicativos reconhecidos', }, splitLongXml: { - "en-us": "Split long lines of XML into multiple lines", - "de-ch": "Teilen Sie lange XML-Zeilen in mehrere Zeilen auf", - "es-es": "Dividir líneas largas de XML en varias líneas", - "fr-fr": "Diviser les longues lignes de XML en plusieurs lignes", - "ru-ru": "Разделить длинные строки XML на несколько строк", - "uk-ua": "Розділіть довгі рядки XML на кілька рядків", - "pt-br": "Dividir longas linhas de XML em várias linhas", + 'en-us': 'Split long lines of XML into multiple lines', + 'de-ch': 'Teilen Sie lange XML-Zeilen in mehrere Zeilen auf', + 'es-es': 'Dividir líneas largas de XML en varias líneas', + 'fr-fr': 'Diviser les longues lignes de XML en plusieurs lignes', + 'ru-ru': 'Разделить длинные строки XML на несколько строк', + 'uk-ua': 'Розділіть довгі рядки XML на кілька рядків', + 'pt-br': 'Dividir longas linhas de XML em várias linhas', }, url: { - "en-us": "URL", - "de-ch": "URL", - "es-es": "URL", - "fr-fr": "URL", - "uk-ua": "URL", - "ru-ru": "URL", - "pt-br": "URL", + 'en-us': 'URL', + 'de-ch': 'URL', + 'es-es': 'URL', + 'fr-fr': 'URL', + 'uk-ua': 'URL', + 'ru-ru': 'URL', + 'pt-br': 'URL', }, pickAttachment: { - "en-us": "Pick an attachment", - "es-es": "Elige un archivo adjunto", - "fr-fr": "Choisissez une pièce jointe", - "ru-ru": "Выберите вложение", - "uk-ua": "Виберіть вкладення", - "de-ch": "Wählen Sie einen Anhang", - "pt-br": "Escolha um anexo", + 'en-us': 'Pick an attachment', + 'es-es': 'Elige un archivo adjunto', + 'fr-fr': 'Choisissez une pièce jointe', + 'ru-ru': 'Выберите вложение', + 'uk-ua': 'Виберіть вкладення', + 'de-ch': 'Wählen Sie einen Anhang', + 'pt-br': 'Escolha um anexo', }, attachmentFailed: { - "en-us": "The attachment failed to load.", - "de-ch": "Der Anhang konnte nicht geladen werden.", - "es-es": "No se pudo cargar el archivo adjunto.", - "fr-fr": "La pièce jointe n'a pas pu être chargée.", - "ru-ru": "Не удалось загрузить вложение.", - "uk-ua": "Не вдалося завантажити вкладений файл.", - "pt-br": "O anexo não pôde ser carregado.", + 'en-us': 'The attachment failed to load.', + 'de-ch': 'Der Anhang konnte nicht geladen werden.', + 'es-es': 'No se pudo cargar el archivo adjunto.', + 'fr-fr': "La pièce jointe n'a pas pu être chargée.", + 'ru-ru': 'Не удалось загрузить вложение.', + 'uk-ua': 'Не вдалося завантажити вкладений файл.', + 'pt-br': 'O anexo não pôde ser carregado.', }, pickImage: { - "en-us": "Pick an image", - "de-ch": "Wählen Sie ein Bild aus", - "es-es": "Elige una imagen", - "fr-fr": "Choisissez une image", - "ru-ru": "Выберите изображение", - "uk-ua": "Виберіть зображення", - "pt-br": "Escolha uma imagem", + 'en-us': 'Pick an image', + 'de-ch': 'Wählen Sie ein Bild aus', + 'es-es': 'Elige una imagen', + 'fr-fr': 'Choisissez une image', + 'ru-ru': 'Выберите изображение', + 'uk-ua': 'Виберіть зображення', + 'pt-br': 'Escolha uma imagem', }, customLogo: { - "en-us": "Expanded Image URL", - "de-ch": "Erweiterte Bild-URL", - "es-es": "URL de imagen expandida", - "fr-fr": "URL de l'image étendue", - "ru-ru": "URL-адрес развернутого изображения", - "uk-ua": "Розширена URL-адреса зображення", - "pt-br": "URL da imagem expandida", + 'en-us': 'Expanded Image URL', + 'de-ch': 'Erweiterte Bild-URL', + 'es-es': 'URL de imagen expandida', + 'fr-fr': "URL de l'image étendue", + 'ru-ru': 'URL-адрес развернутого изображения', + 'uk-ua': 'Розширена URL-адреса зображення', + 'pt-br': 'URL da imagem expandida', }, customLogoCollapsed: { - "en-us": "Collapsed Image URL", - "de-ch": "URL des minimierten Bildes", - "es-es": "URL de imagen contraída", - "fr-fr": "URL de l'image réduite", - "ru-ru": "URL-адрес свернутого изображения", - "uk-ua": "URL-адреса згорнутого зображення", - "pt-br": "URL da imagem recolhida", + 'en-us': 'Collapsed Image URL', + 'de-ch': 'URL des minimierten Bildes', + 'es-es': 'URL de imagen contraída', + 'fr-fr': "URL de l'image réduite", + 'ru-ru': 'URL-адрес свернутого изображения', + 'uk-ua': 'URL-адреса згорнутого зображення', + 'pt-br': 'URL da imagem recolhida', }, customLogoDescription: { - "en-us": - "A URL to an image that would be displayed next to the Specify logo in the navigation menu.", - "de-ch": - "Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.", - "es-es": - "Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.", - "fr-fr": - "Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.", - "ru-ru": - "URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.", - "uk-ua": - "URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.", - "pt-br": - "Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.", + 'en-us': + 'A URL to an image that would be displayed next to the Specify logo in the navigation menu.', + 'de-ch': + 'Eine URL zu einem Bild, das neben dem angegebenen Logo im Navigationsmenü angezeigt wird.', + 'es-es': + 'Una URL a una imagen que se mostrará junto al logotipo Especificar en el menú de navegación.', + 'fr-fr': + 'Une URL vers une image qui serait affichée à côté du logo Specify dans le menu de navigation.', + 'ru-ru': + 'URL-адрес изображения, которое будет отображаться рядом с логотипом «Укажите» в меню навигации.', + 'uk-ua': + 'URL-адреса зображення, яке відображатиметься поруч із «Вказати логотип» у меню навігації.', + 'pt-br': + 'Um URL para uma imagem que seria exibida ao lado do logotipo Especificar no menu de navegação.', }, showLineNumber: { - "en-us": "Show query result line number", - "de-ch": "Zeilennummer des Abfrageergebnisses anzeigen", - "es-es": "Mostrar el número de línea del resultado de la consulta", - "fr-fr": "Afficher le numéro de ligne du résultat de la requête", - "ru-ru": "Показать номер строки результата запроса", - "uk-ua": "Показати номер рядка результату запиту", - "pt-br": "Mostrar número da linha do resultado da consulta", + 'en-us': 'Show query result line number', + 'de-ch': 'Zeilennummer des Abfrageergebnisses anzeigen', + 'es-es': 'Mostrar el número de línea del resultado de la consulta', + 'fr-fr': 'Afficher le numéro de ligne du résultat de la requête', + 'ru-ru': 'Показать номер строки результата запроса', + 'uk-ua': 'Показати номер рядка результату запиту', + 'pt-br': 'Mostrar número da linha do resultado da consulta', }, saveButtonColor: { - "en-us": "Save button color", - "de-ch": "Farbe der Schaltfläche „Speichern“", - "es-es": "Guardar el color del botón", - "fr-fr": "Couleur du bouton Enregistrer", - "ru-ru": "Сохранить цвет кнопки", - "uk-ua": "Зберегти колір кнопки", - "pt-br": "Cor do botão Salvar", + 'en-us': 'Save button color', + 'de-ch': 'Farbe der Schaltfläche „Speichern“', + 'es-es': 'Guardar el color del botón', + 'fr-fr': 'Couleur du bouton Enregistrer', + 'ru-ru': 'Сохранить цвет кнопки', + 'uk-ua': 'Зберегти колір кнопки', + 'pt-br': 'Cor do botão Salvar', }, secondaryButtonColor: { - "en-us": "Secondary button color", - "es-es": "Color del botón secundario", - "fr-fr": "Couleur du bouton secondaire", - "ru-ru": "Цвет вторичной кнопки", - "uk-ua": "Колір вторинної кнопки", - "de-ch": "Sekundäre Schaltflächenfarbe", - "pt-br": "Cor do botão secundário", + 'en-us': 'Secondary button color', + 'es-es': 'Color del botón secundario', + 'fr-fr': 'Couleur du bouton secondaire', + 'ru-ru': 'Цвет вторичной кнопки', + 'uk-ua': 'Колір вторинної кнопки', + 'de-ch': 'Sekundäre Schaltflächenfarbe', + 'pt-br': 'Cor do botão secundário', }, secondaryLightButtonColor: { - "en-us": "Secondary light button color", - "de-ch": "Farbe der sekundären Lichttaste", - "es-es": "Color del botón de luz secundaria", - "fr-fr": "Couleur du bouton lumineux secondaire", - "ru-ru": "Цвет кнопки дополнительного освещения", - "uk-ua": "Колір вторинної світлової кнопки", - "pt-br": "Cor do botão de luz secundária", + 'en-us': 'Secondary light button color', + 'de-ch': 'Farbe der sekundären Lichttaste', + 'es-es': 'Color del botón de luz secundaria', + 'fr-fr': 'Couleur du bouton lumineux secondaire', + 'ru-ru': 'Цвет кнопки дополнительного освещения', + 'uk-ua': 'Колір вторинної світлової кнопки', + 'pt-br': 'Cor do botão de luz secundária', }, dangerButtonColor: { - "en-us": "Danger button color", - "de-ch": "Farbe der Gefahrenschaltfläche", - "es-es": "Color del botón de peligro", - "fr-fr": "Couleur du bouton de danger", - "ru-ru": "Цвет кнопки «Опасность»", - "uk-ua": "Колір кнопки небезпеки", - "pt-br": "Cor do botão de perigo", + 'en-us': 'Danger button color', + 'de-ch': 'Farbe der Gefahrenschaltfläche', + 'es-es': 'Color del botón de peligro', + 'fr-fr': 'Couleur du bouton de danger', + 'ru-ru': 'Цвет кнопки «Опасность»', + 'uk-ua': 'Колір кнопки небезпеки', + 'pt-br': 'Cor do botão de perigo', }, infoButtonColor: { - "en-us": "Info button color", - "de-ch": "Farbe der Info-Schaltfläche", - "es-es": "Color del botón de información", - "fr-fr": "Couleur du bouton d'information", - "ru-ru": "Цвет кнопки информации", - "uk-ua": "Колір інформаційної кнопки", - "pt-br": "Cor do botão de informações", + 'en-us': 'Info button color', + 'de-ch': 'Farbe der Info-Schaltfläche', + 'es-es': 'Color del botón de información', + 'fr-fr': "Couleur du bouton d'information", + 'ru-ru': 'Цвет кнопки информации', + 'uk-ua': 'Колір інформаційної кнопки', + 'pt-br': 'Cor do botão de informações', }, warningButtonColor: { - "en-us": "Warning button color", - "de-ch": "Farbe der Warnschaltfläche", - "es-es": "Color del botón de advertencia", - "fr-fr": "Couleur du bouton d'avertissement", - "ru-ru": "Цвет кнопки предупреждения", - "uk-ua": "Колір кнопки попередження", - "pt-br": "Cor do botão de aviso", + 'en-us': 'Warning button color', + 'de-ch': 'Farbe der Warnschaltfläche', + 'es-es': 'Color del botón de advertencia', + 'fr-fr': "Couleur du bouton d'avertissement", + 'ru-ru': 'Цвет кнопки предупреждения', + 'uk-ua': 'Колір кнопки попередження', + 'pt-br': 'Cor do botão de aviso', }, successButtonColor: { - "en-us": "Success button color", - "de-ch": "Farbe der Schaltfläche „Erfolg“", - "es-es": "Color del botón de éxito", - "fr-fr": "Couleur du bouton de réussite", - "ru-ru": "Цвет кнопки «Успех»", - "uk-ua": "Колір кнопки успіху", - "pt-br": "Cor do botão de sucesso", + 'en-us': 'Success button color', + 'de-ch': 'Farbe der Schaltfläche „Erfolg“', + 'es-es': 'Color del botón de éxito', + 'fr-fr': 'Couleur du bouton de réussite', + 'ru-ru': 'Цвет кнопки «Успех»', + 'uk-ua': 'Колір кнопки успіху', + 'pt-br': 'Cor do botão de sucesso', }, openAsReadOnly: { - "en-us": "Open all records in read-only mode", - "de-ch": "Alle Datensätze im schreibgeschützten Modus öffnen", - "es-es": "Abrir todos los registros en modo de solo lectura", - "fr-fr": "Ouvrir tous les enregistrements en mode lecture seule", - "ru-ru": "Открыть все записи в режиме только для чтения", - "uk-ua": "Відкрити всі записи в режимі лише для читання", - "pt-br": "Abra todos os registros no modo somente leitura", + 'en-us': 'Open all records in read-only mode', + 'de-ch': 'Alle Datensätze im schreibgeschützten Modus öffnen', + 'es-es': 'Abrir todos los registros en modo de solo lectura', + 'fr-fr': 'Ouvrir tous les enregistrements en mode lecture seule', + 'ru-ru': 'Открыть все записи в режиме только для чтения', + 'uk-ua': 'Відкрити всі записи в режимі лише для читання', + 'pt-br': 'Abra todos os registros no modo somente leitura', }, displayBasicView: { - "en-us": "Display basic view", - "de-ch": "Basisansicht anzeigen", - "es-es": "Mostrar vista básica", - "fr-fr": "Afficher la vue de base", - "ru-ru": "Отобразить базовый вид", - "uk-ua": "Відобразити базовий вигляд", - "pt-br": "Exibir visualização básica", + 'en-us': 'Display basic view', + 'de-ch': 'Basisansicht anzeigen', + 'es-es': 'Mostrar vista básica', + 'fr-fr': 'Afficher la vue de base', + 'ru-ru': 'Отобразить базовый вид', + 'uk-ua': 'Відобразити базовий вигляд', + 'pt-br': 'Exibir visualização básica', }, showComparisonOperatorsForString: { - "en-us": "Show comparison operators for text-based fields", - "de-ch": "Vergleichsoperatoren für textbasierte Felder anzeigen", - "es-es": "Mostrar operadores de comparación para campos basados en texto", - "fr-fr": "Afficher les opérateurs de comparaison pour les champs textuels", - "pt-br": "Mostrar operadores de comparação para campos baseados em texto", - "ru-ru": "Показать операторы сравнения для текстовых полей", - "uk-ua": "Показати оператори порівняння для текстових полів", + 'en-us': 'Show comparison operators for text-based fields', + 'de-ch': 'Vergleichsoperatoren für textbasierte Felder anzeigen', + 'es-es': 'Mostrar operadores de comparación para campos basados en texto', + 'fr-fr': 'Afficher les opérateurs de comparaison pour les champs textuels', + 'pt-br': 'Mostrar operadores de comparação para campos baseados em texto', + 'ru-ru': 'Показать операторы сравнения для текстовых полей', + 'uk-ua': 'Показати оператори порівняння для текстових полів', }, showComparisonOperatorsDescription: { - "en-us": - "Allows the following filters to apply to text fields: Greater Than, Less Than, Greater Than or Equal to, and Less Than or Equal to", - "de-ch": - "Ermöglicht die Anwendung der folgenden Filter auf Textfelder: Größer als, Kleiner als, Größer als oder gleich und Kleiner als oder gleich", - "es-es": - "Permite aplicar los siguientes filtros a los campos de texto: Mayor que, Menor que, Mayor o igual que y Menor o igual que", - "fr-fr": + 'en-us': + 'Allows the following filters to apply to text fields: Greater Than, Less Than, Greater Than or Equal to, and Less Than or Equal to', + 'de-ch': + 'Ermöglicht die Anwendung der folgenden Filter auf Textfelder: Größer als, Kleiner als, Größer als oder gleich und Kleiner als oder gleich', + 'es-es': + 'Permite aplicar los siguientes filtros a los campos de texto: Mayor que, Menor que, Mayor o igual que y Menor o igual que', + 'fr-fr': "Permet d'appliquer les filtres suivants aux champs de texte : Supérieur à, Inférieur à, Supérieur ou égal à et Inférieur ou égal à", - "pt-br": - "Permite que os seguintes filtros sejam aplicados aos campos de texto: Maior que, Menor que, Maior ou igual a e Menor ou igual a", - "ru-ru": - "Позволяет применять к текстовым полям следующие фильтры: «Больше», «Меньше», «Больше или равно» и «Меньше или равно».", - "uk-ua": - "Дозволяє застосовувати до текстових полів такі фільтри: «Більше ніж», «Менше ніж», «Більше або дорівнює» та «Менше або дорівнює»", + 'pt-br': + 'Permite que os seguintes filtros sejam aplicados aos campos de texto: Maior que, Menor que, Maior ou igual a e Menor ou igual a', + 'ru-ru': + 'Позволяет применять к текстовым полям следующие фильтры: «Больше», «Меньше», «Больше или равно» и «Меньше или равно».', + 'uk-ua': + 'Дозволяє застосовувати до текстових полів такі фільтри: «Більше ніж», «Менше ніж», «Більше або дорівнює» та «Менше або дорівнює»', }, basicView: { - "en-us": "Basic view", - "de-ch": "Basisansicht", - "es-es": "Vista básica", - "fr-fr": "Vue de base", - "ru-ru": "Базовый вид", - "uk-ua": "Основний вигляд", - "pt-br": "Visão básica", + 'en-us': 'Basic view', + 'de-ch': 'Basisansicht', + 'es-es': 'Vista básica', + 'fr-fr': 'Vue de base', + 'ru-ru': 'Базовый вид', + 'uk-ua': 'Основний вигляд', + 'pt-br': 'Visão básica', }, detailedView: { - "en-us": "Detailed view", - "de-ch": "Detailansicht", - "es-es": "Vista detallada", - "fr-fr": "Vue détaillée", - "ru-ru": "Подробный вид", - "uk-ua": "Детальний вигляд", - "pt-br": "Visão detalhada", + 'en-us': 'Detailed view', + 'de-ch': 'Detailansicht', + 'es-es': 'Vista detallada', + 'fr-fr': 'Vue détaillée', + 'ru-ru': 'Подробный вид', + 'uk-ua': 'Детальний вигляд', + 'pt-br': 'Visão detalhada', }, attachmentPreviewMode: { - "en-us": "Attachment preview mode", - "de-ch": "Anhangsvorschaumodus", - "es-es": "Modo de vista previa de archivos adjuntos", - "fr-fr": "Mode d'aperçu des pièces jointes", - "ru-ru": "Режим предварительного просмотра вложений", - "uk-ua": "Режим попереднього перегляду вкладених файлів", - "pt-br": "Modo de visualização de anexos", + 'en-us': 'Attachment preview mode', + 'de-ch': 'Anhangsvorschaumodus', + 'es-es': 'Modo de vista previa de archivos adjuntos', + 'fr-fr': "Mode d'aperçu des pièces jointes", + 'ru-ru': 'Режим предварительного просмотра вложений', + 'uk-ua': 'Режим попереднього перегляду вкладених файлів', + 'pt-br': 'Modo de visualização de anexos', }, fullResolution: { - "en-us": "Full Resolution", - "de-ch": "Volle Auflösung", - "es-es": "Resolución completa", - "fr-fr": "Pleine résolution", - "ru-ru": "Полное разрешение", - "uk-ua": "Повна роздільна здатність", - "pt-br": "Resolução completa", + 'en-us': 'Full Resolution', + 'de-ch': 'Volle Auflösung', + 'es-es': 'Resolución completa', + 'fr-fr': 'Pleine résolution', + 'ru-ru': 'Полное разрешение', + 'uk-ua': 'Повна роздільна здатність', + 'pt-br': 'Resolução completa', }, thumbnail: { - "en-us": "Thumbnail", - "de-ch": "Miniaturansicht", - "es-es": "Uña del pulgar", - "fr-fr": "Vignette", - "ru-ru": "Миниатюра", - "uk-ua": "Мініатюра", - "pt-br": "Miniatura", + 'en-us': 'Thumbnail', + 'de-ch': 'Miniaturansicht', + 'es-es': 'Uña del pulgar', + 'fr-fr': 'Vignette', + 'ru-ru': 'Миниатюра', + 'uk-ua': 'Мініатюра', + 'pt-br': 'Miniatura', }, addSearchBarHomePage: { - "en-us": "Add Search Bar on home page", - "de-ch": "Suchleiste auf der Startseite hinzufügen", - "es-es": "Agregar barra de búsqueda en la página de inicio", - "fr-fr": "Ajouter une barre de recherche sur la page d'accueil", - "ru-ru": "Добавить панель поиска на домашнюю страницу", - "uk-ua": "Додайте рядок пошуку на головну сторінку", - "pt-br": "Adicionar barra de pesquisa na página inicial", + 'en-us': 'Add Search Bar on home page', + 'de-ch': 'Suchleiste auf der Startseite hinzufügen', + 'es-es': 'Agregar barra de búsqueda en la página de inicio', + 'fr-fr': "Ajouter une barre de recherche sur la page d'accueil", + 'ru-ru': 'Добавить панель поиска на домашнюю страницу', + 'uk-ua': 'Додайте рядок пошуку на головну сторінку', + 'pt-br': 'Adicionar barra de pesquisa na página inicial', }, inheritanceCatNumberPref: { - "en-us": - "Enable the inheritance of the primary catalog number to its empty siblings.", - "de-ch": - "Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.", - "es-es": - "Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.", - "fr-fr": + 'en-us': + 'Enable the inheritance of the primary catalog number to its empty siblings.', + 'de-ch': + 'Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.', + 'es-es': + 'Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.', + 'fr-fr': "Activez l'héritage du numéro de catalogue principal vers ses frères vides.", - "pt-br": - "Habilitar a herança do número do catálogo primário para seus irmãos vazios.", - "ru-ru": - "Включить наследование основного каталожного номера его пустыми родственными номерами.", - "uk-ua": - "Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.", + 'pt-br': + 'Habilitar a herança do número do catálogo primário para seus irmãos vazios.', + 'ru-ru': + 'Включить наследование основного каталожного номера его пустыми родственными номерами.', + 'uk-ua': + 'Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.', }, inheritanceCatNumberParentCOPref: { - "en-us": - "Enable the inheritance of the parent catalog number to its empty children.", - "de-ch": - "Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.", - "es-es": - "Habilitar la herencia del número de catálogo padre a sus hijos vacíos.", - "fr-fr": + 'en-us': + 'Enable the inheritance of the parent catalog number to its empty children.', + 'de-ch': + 'Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.', + 'es-es': + 'Habilitar la herencia del número de catálogo padre a sus hijos vacíos.', + 'fr-fr': "Activer l'héritage du numéro de catalogue parent à ses enfants vides.", - "pt-br": - "Habilitar a herança do número do catálogo pai para seus filhos vazios.", - "ru-ru": - "Включить наследование родительского каталожного номера его пустыми дочерними элементами.", - "uk-ua": - "Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.", + 'pt-br': + 'Habilitar a herança do número do catálogo pai para seus filhos vazios.', + 'ru-ru': + 'Включить наследование родительского каталожного номера его пустыми дочерними элементами.', + 'uk-ua': + 'Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.', }, } as const; - export const preferencesGeneralText = createDictionary( preferencesGeneralDictionary ); diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.ts b/specifyweb/frontend/js_src/lib/localization/preferences.ts index bed614cba81..0bf175a8b64 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.ts @@ -3,10 +3,10 @@ * * @module */ -import { createDictionary } from './utils'; import { preferencesBehaviorDictionary } from './preferences.behavior'; import { preferencesContentDictionary } from './preferences.content'; import { preferencesGeneralDictionary } from './preferences.general'; +import { createDictionary } from './utils'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file const preferencesDictionary = { ...preferencesGeneralDictionary, diff --git a/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts b/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts index 08bb7910ec0..88bc5da41e6 100644 --- a/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts +++ b/specifyweb/frontend/js_src/lib/localization/utils/scanUsages.ts @@ -168,11 +168,9 @@ export async function scanUsages( categoryName, strings: Object.fromEntries( Object.keys(strings).map((key) => { - const rawStrings = - (Reflect.getOwnPropertyDescriptor(strings, key)?.value ?? - (strings as LanguageDictionary)[ - key as keyof LanguageDictionary - ]) as LocalizationEntry; + const rawStrings = (Reflect.getOwnPropertyDescriptor(strings, key) + ?.value ?? + (strings as LanguageDictionary)[key]) as LocalizationEntry; Object.keys(rawStrings) .filter((language) => !f.has(expectedKeys, language)) From 9c4bc18936f998bd7f5d7a1fe518a2c2e5dee5d6 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:15:42 -0500 Subject: [PATCH 064/100] refactor(preferences): improve tree management --- .../Preferences/CollectionDefinitions.tsx | 20 +++++++++++----- .../lib/localization/preferences.behavior.ts | 18 +++++++------- .../frontend/js_src/lib/localization/tree.ts | 24 +++---------------- 3 files changed, 26 insertions(+), 36 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index ed10364a63a..fda34b26545 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -2,6 +2,8 @@ * Definitions for Collection preferences */ +import type { LocalizedString } from 'typesafe-i18n'; + import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; @@ -11,10 +13,16 @@ import { treeText } from '../../localization/tree'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure } from '../../utils/types'; +import { camelToHuman } from '../../utils/utils'; import type { StatLayout } from '../Statistics/types'; +import { genericTables } from '../DataModel/tables'; +import type { Tables } from '../DataModel/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; +const tableLabel = (tableName: keyof Tables): LocalizedString => + genericTables[tableName]?.label ?? camelToHuman(tableName); + const specifyNetworkItems = { publishingOrganization: definePref({ title: specifyNetworkText.publishingOrganizationKey(), @@ -74,7 +82,7 @@ export const collectionPreferenceDefinitions = { items: { 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': definePref({ - title: treeText.allowSynonymizedGeologicTimePeriodChildren(), + title: () => tableLabel('GeologicTimePeriod'), requiresReload: false, visible: true, defaultValue: false, @@ -82,7 +90,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Taxon': definePref({ - title: treeText.allowSynonymizedTaxonChildren(), + title: () => tableLabel('Taxon'), requiresReload: false, visible: true, defaultValue: false, @@ -90,7 +98,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Geography': definePref({ - title: treeText.allowSynonymizedGeographyChildren(), + title: () => tableLabel('Geography'), requiresReload: false, visible: true, defaultValue: false, @@ -98,7 +106,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.LithoStrat': definePref({ - title: treeText.allowSynonymizedLithostratChildren(), + title: () => tableLabel('LithoStrat'), requiresReload: false, visible: true, defaultValue: false, @@ -106,7 +114,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.Storage': definePref({ - title: treeText.allowSynonymizedStorageChildren(), + title: () => tableLabel('Storage'), requiresReload: false, visible: true, defaultValue: false, @@ -114,7 +122,7 @@ export const collectionPreferenceDefinitions = { }), 'sp7.allow_adding_child_to_synonymized_parent.TectonicUnit': definePref({ - title: treeText.allowSynonymizedTectonicUnitChildren(), + title: () => tableLabel('TectonicUnit'), requiresReload: false, visible: true, defaultValue: false, diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index df9608745f5..7f7b9efb1af 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -442,13 +442,13 @@ export const preferencesBehaviorDictionary = { 'Você pode mover na direção oposta pressionando Shift+Enter.', }, filterPickLists: { - 'en-us': 'Filter pick list items', - 'ru-ru': 'Фильтрация элементов списка выбора', - 'es-es': 'Filtrar elementos de la lista de selección', - 'fr-fr': 'Filtrer les éléments de la liste de sélection', - 'uk-ua': 'Фільтр вибору елементів списку', - 'de-ch': 'Auswahllistenelemente filtern', - 'pt-br': 'Filtrar itens da lista de seleção', + 'en-us': 'Pick List Filtering', + 'ru-ru': 'Фильтрация списка выбора', + 'es-es': 'Filtrado de lista de selección', + 'fr-fr': 'Filtrage de la liste de sélection', + 'uk-ua': 'Фільтрація списку вибору', + 'de-ch': 'Picklistenfilterung', + 'pt-br': 'Filtragem de lista de seleção', }, exportFileDelimiter: { 'en-us': 'Export file delimiter', @@ -687,11 +687,11 @@ export const preferencesBehaviorDictionary = { 'pt-br': 'Fechar com clique externo', }, scopeEntireTablePicklists: { - 'en-us': 'Scope "Entire Table" picklists', + 'en-us': 'Scope "Entire Table" Picklists', }, scopeEntireTablePicklistsDescription: { 'en-us': - 'Restrict "Entire Table" picklists to values used by records in this collection.', + 'If enabled, picklists of type "Entire Table" will only show items that are in use within the current collection.', }, catalogNumberInheritanceDescription: { 'en-us': diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 667f6af34bc..7b9993f4bb2 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -704,31 +704,13 @@ export const treeText = createDictionary({ 'uk-ua': 'Метеорити', }, treeManagement: { - 'en-us': 'Tree management', + 'en-us': 'Tree Management', }, synonymizedNodes: { - 'en-us': 'Synonymized nodes', + 'en-us': 'Synonym Behavior', }, synonymizedNodesDescription: { 'en-us': - 'Allow creating children under synonymized nodes in specific trees.', - }, - allowSynonymizedGeologicTimePeriodChildren: { - 'en-us': 'Allow children under synonymized Geologic Time Period nodes', - }, - allowSynonymizedTaxonChildren: { - 'en-us': 'Allow children under synonymized Taxon nodes', - }, - allowSynonymizedGeographyChildren: { - 'en-us': 'Allow children under synonymized Geography nodes', - }, - allowSynonymizedLithostratChildren: { - 'en-us': 'Allow children under synonymized Lithostratigraphy nodes', - }, - allowSynonymizedStorageChildren: { - 'en-us': 'Allow children under synonymized Storage nodes', - }, - allowSynonymizedTectonicUnitChildren: { - 'en-us': 'Allow children under synonymized Tectonic Unit nodes', + 'If enabled, this allows users to add children to synonymized parents and to synonymize a node with children.', }, } as const); From 2d5870c0ba86e20e44587081a6c8f4489b9a58aa Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:16:09 -0500 Subject: [PATCH 065/100] fix: improve public attachment description --- specifyweb/frontend/js_src/lib/localization/attachments.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index 81100afff64..c969db23a3a 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -678,10 +678,10 @@ export const attachmentsText = createDictionary({ 'uk-ua': 'Видалення вкладень', }, publicDefault: { - 'en-us': 'New attachments are public', + 'en-us': 'Make Attachments Public by Default', }, publicDefaultDescription: { 'en-us': - 'Set the default visibility for attachments created within this collection.', + 'This controls whether or not new attachments added to this collection are flagged as "Public" by default. Public attachments will be visible for everyone viewing a linked Specify Web Portal. This setting can be overridden on a per-attachment basis and does not affect existing attachments.', }, } as const); From 8414c82205680b9470175ac6c80ad65f8e7de852 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:16:50 -0500 Subject: [PATCH 066/100] Update attachments.ts --- specifyweb/frontend/js_src/lib/localization/attachments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index c969db23a3a..50268c30a30 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -682,6 +682,6 @@ export const attachmentsText = createDictionary({ }, publicDefaultDescription: { 'en-us': - 'This controls whether or not new attachments added to this collection are flagged as "Public" by default. Public attachments will be visible for everyone viewing a linked Specify Web Portal. This setting can be overridden on a per-attachment basis and does not affect existing attachments.', + 'This controls whether or not new attachments added to this collection are flagged as "Public" by default. Public attachments will automatically be visible on a Specify Web Portal. This setting can be overridden on a per-attachment basis and does not affect existing attachments.', }, } as const); From 69986f54b6cbc2ec6a68bfebf6b81bb066739592 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:24:03 -0500 Subject: [PATCH 067/100] feat: add descriptions for specify network fields --- .../components/Preferences/CollectionDefinitions.tsx | 2 ++ .../js_src/lib/localization/specifyNetwork.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index fda34b26545..786a8f1e586 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -26,6 +26,7 @@ const tableLabel = (tableName: keyof Tables): LocalizedString => const specifyNetworkItems = { publishingOrganization: definePref({ title: specifyNetworkText.publishingOrganizationKey(), + description: specifyNetworkText.publishingOrganizationKeyDescription(), requiresReload: false, visible: true, defaultValue: undefined, @@ -33,6 +34,7 @@ const specifyNetworkItems = { }), collectionKey: definePref({ title: specifyNetworkText.collectionKey(), + description: specifyNetworkText.collectionKeyDescription(), requiresReload: false, visible: true, defaultValue: undefined, diff --git a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts index 37b3516e2e6..08d37debb90 100644 --- a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts +++ b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts @@ -17,10 +17,18 @@ export const specifyNetworkText = createDictionary({ 'pt-br': 'Especificar rede', }, publishingOrganizationKey: { - 'en-us': 'Publishing organization key', + 'en-us': 'GBIF Publishing Organization Key', + }, + publishingOrganizationKeyDescription: { + 'en-us': + 'The GBIF "publishingOrgKey" (a UUID) for this collection, used for Specify Network integration.', }, collectionKey: { - 'en-us': 'Collection key', + 'en-us': 'GBIF Data Set Key', + }, + collectionKeyDescription: { + 'en-us': + 'The GBIF "dataSetKey" (a UUID) for this collection, used for Specify Network integration.', }, occurrenceOrGuidRequired: { 'en-us': 'Species Name or GUID must be provided to display this page', From 94d499a2fa68b7909bd34186f23246c9995d3b31 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:24:15 -0500 Subject: [PATCH 068/100] feat: improve descriptions for stats fields --- specifyweb/frontend/js_src/lib/localization/stats.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/stats.tsx b/specifyweb/frontend/js_src/lib/localization/stats.tsx index 9ac2e816daf..e531316937b 100644 --- a/specifyweb/frontend/js_src/lib/localization/stats.tsx +++ b/specifyweb/frontend/js_src/lib/localization/stats.tsx @@ -317,17 +317,17 @@ export const statsText = createDictionary({ 'en-us': 'Defines the layout of the statistics page', }, showPreparationsTotal: { - 'en-us': 'Show preparation totals', + 'en-us': 'Show Preparations Totals', }, showPreparationsTotalDescription: { 'en-us': - 'Include an overall total across preparation types on the statistics page.', + 'If enabled, the default Preparations statistics panel will include a total count for each preparation of a particular preparation type alongside the overall total. This is useful for lot-based collections.', }, autoRefreshRate: { - 'en-us': 'Auto-refresh rate (hours)', + 'en-us': 'Auto-Refresh Rate (Hours)', }, autoRefreshRateDescription: { - 'en-us': 'Specify how frequently shared statistics refresh their data.', + 'en-us': 'The time interval, in hours, at which the statistics page will automatically refresh its data. Default is 24.', }, }); /* eslint-enable @typescript-eslint/naming-convention */ From 0e9f9c223029a1d8e16d653a0d48c6347bac8278 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:24:57 -0500 Subject: [PATCH 069/100] feat: improve title for synonym behavior section --- specifyweb/frontend/js_src/lib/localization/tree.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 7b9993f4bb2..4a3f2737142 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -707,7 +707,7 @@ export const treeText = createDictionary({ 'en-us': 'Tree Management', }, synonymizedNodes: { - 'en-us': 'Synonym Behavior', + 'en-us': 'Expand Synonym Behavior', }, synonymizedNodesDescription: { 'en-us': From a9133aa5b3b301cdf32de40c1ffe81e5547968ad Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 14:51:36 -0500 Subject: [PATCH 070/100] feat: improve cat number inheritance --- .../Preferences/CollectionDefinitions.tsx | 20 ++++------ .../lib/localization/preferences.behavior.ts | 40 +++++-------------- .../frontend/js_src/lib/localization/tree.ts | 4 ++ 3 files changed, 22 insertions(+), 42 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 786a8f1e586..ffedbb29c31 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -200,13 +200,13 @@ export const collectionPreferenceDefinitions = { catalogNumberInheritance: { title: queryText.catalogNumberInheritance(), - description: preferencesText.catalogNumberInheritanceDescription(), subCategories: { - behavior: { - title: preferencesText.behavior(), + collectionObject: { + title: () => tableLabel('CollectionObjectGroup'), items: { inheritance: definePref({ title: preferencesText.inheritanceCatNumberPref(), + description: preferencesText.inheritanceCatNumberPrefDescription(), requiresReload: false, visible: true, defaultValue: false, @@ -214,18 +214,12 @@ export const collectionPreferenceDefinitions = { }), }, }, - }, - }, - - catalogNumberParentInheritance: { - title: queryText.catalogNumberParentCOInheritance(), - description: preferencesText.catalogNumberParentInheritanceDescription(), - subCategories: { - behavior: { - title: preferencesText.behavior(), + component: { + title: () => camelToHuman('Component'), items: { inheritance: definePref({ title: preferencesText.inheritanceCatNumberParentCOPref(), + description: preferencesText.inheritanceCatNumberParentCOPrefDescription(), requiresReload: false, visible: true, defaultValue: false, @@ -236,6 +230,8 @@ export const collectionPreferenceDefinitions = { }, }, + /* catalogNumberParentInheritance removed - folded into catalogNumberInheritance */ + uniqueCatalogNumberAccrossComponentAndCO: { title: queryText.uniqueCatalogNumberAcrossComponentAndCo(), subCategories: { diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 7f7b9efb1af..7ea667010ed 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -693,10 +693,6 @@ export const preferencesBehaviorDictionary = { 'en-us': 'If enabled, picklists of type "Entire Table" will only show items that are in use within the current collection.', }, - catalogNumberInheritanceDescription: { - 'en-us': - 'Configure whether sibling Collection Objects and their child Collection Objects inherit catalog numbers from the primary or parent record.', - }, catalogNumberParentInheritanceDescription: { 'en-us': 'Control whether component records inherit catalog numbers from their parent Collection Object.', @@ -1142,35 +1138,19 @@ altClickToSupressNewTab: { }, inheritanceCatNumberPref: { 'en-us': - 'Enable the inheritance of the primary catalog number to its empty siblings.', - 'de-ch': - 'Aktivieren Sie die Vererbung der primären Katalognummer an ihre leeren Geschwister.', - 'es-es': - 'Habilitar la herencia del número de catálogo principal a sus hermanos vacíos.', - 'fr-fr': - "Activer l'héritage du numéro de catalogue principal à ses frères vides.", - 'pt-br': - 'Habilitar a herança do número de catálogo primário para seus irmãos vazios.', - 'ru-ru': - 'Включить наследование основного каталожного номера его пустыми родственными номерами.', - 'uk-ua': - 'Увімкнути успадкування основного каталожного номера його порожнім братам і сестрам.', + 'Enable Catalog Number Inheritance From Primary CO', + }, + inheritanceCatNumberPrefDescription: { + 'en-us': + 'Control whether component records inherit catalog numbers from their primary Collection Object.', }, inheritanceCatNumberParentCOPref: { 'en-us': - 'Enable the inheritance of the parent catalog number to its empty children.', - 'de-ch': - 'Aktivieren Sie die Vererbung der übergeordneten Katalognummer an ihre leeren untergeordneten Elemente.', - 'es-es': - 'Habilitar la herencia del número de catálogo padre a sus hijos vacíos.', - 'fr-fr': - "Activer l'héritage du numéro de catalogue parent à ses enfants vides.", - 'pt-br': - 'Habilita a herança do número do catálogo pai para seus filhos vazios.', - 'ru-ru': - 'Включить наследование родительского каталожного номера его пустыми дочерними элементами.', - 'uk-ua': - 'Увімкнути успадкування батьківського каталожного номера його порожнім дочірнім елементам.', + 'Enable Catalog Number Inheritance From Parent CO', + }, + inheritanceCatNumberParentCOPrefDescription: { + 'en-us': + 'Control whether component records inherit catalog numbers from their parent Collection Object.', }, uniqueCatNumberAcrossCompAndCo: { 'en-us': diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 4a3f2737142..d41144cffcc 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -713,4 +713,8 @@ export const treeText = createDictionary({ 'en-us': 'If enabled, this allows users to add children to synonymized parents and to synonymize a node with children.', }, + synonymizedNodesPermissionDescription: { + 'en-us': + 'Enable adding children to synonymized parents on this tree. When enabled, users may add child nodes beneath parents that have been marked as synonyms.', + }, } as const); From 2ca038bd598ef3f7416ccafd57a3df2c6cb01493 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:10:12 -0500 Subject: [PATCH 071/100] feat: improve descriptions for catalog number inheritance --- .../Preferences/CollectionDefinitions.tsx | 38 +++++++++++++++++-- .../lib/localization/preferences.behavior.ts | 8 ++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index ffedbb29c31..449b1ed2be8 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -19,6 +19,8 @@ import { genericTables } from '../DataModel/tables'; import type { Tables } from '../DataModel/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; +import { tables } from '../DataModel/tables'; +import { getField } from '../DataModel/helpers'; const tableLabel = (tableName: keyof Tables): LocalizedString => genericTables[tableName]?.label ?? camelToHuman(tableName); @@ -205,8 +207,22 @@ export const collectionPreferenceDefinitions = { title: () => tableLabel('CollectionObjectGroup'), items: { inheritance: definePref({ - title: preferencesText.inheritanceCatNumberPref(), - description: preferencesText.inheritanceCatNumberPrefDescription(), + title: () => + preferencesText.inheritanceCatNumberPref({ + catalogNumber: getField( + tables.CollectionObject, + 'catalogNumber' + ).label, + collectionObject: tables.CollectionObject.label, + }), + description: () => + preferencesText.inheritanceCatNumberPrefDescription({ + catalogNumber: getField( + tables.CollectionObject, + 'catalogNumber' + ).label, + collectionObject: tables.CollectionObject.label, + }), requiresReload: false, visible: true, defaultValue: false, @@ -218,8 +234,22 @@ export const collectionPreferenceDefinitions = { title: () => camelToHuman('Component'), items: { inheritance: definePref({ - title: preferencesText.inheritanceCatNumberParentCOPref(), - description: preferencesText.inheritanceCatNumberParentCOPrefDescription(), + title: () => + preferencesText.inheritanceCatNumberPref({ + catalogNumber: getField( + tables.CollectionObject, + 'catalogNumber' + ).label, + collectionObject: tables.CollectionObject.label, + }), + description: () => + preferencesText.inheritanceCatNumberParentCOPrefDescription({ + catalogNumber: getField( + tables.CollectionObject, + 'catalogNumber' + ).label, + collectionObject: tables.CollectionObject.label, + }), requiresReload: false, visible: true, defaultValue: false, diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 7ea667010ed..9b0cf0e6901 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -1138,19 +1138,19 @@ altClickToSupressNewTab: { }, inheritanceCatNumberPref: { 'en-us': - 'Enable Catalog Number Inheritance From Primary CO', + 'Enable {catalogNumber:string} Inheritance From Primary {collectionObject:string}', }, inheritanceCatNumberPrefDescription: { 'en-us': - 'Control whether component records inherit catalog numbers from their primary Collection Object.', + 'Control whether component records inherit the {catalogNumber:string} from the primary {collectionObject:string}.', }, inheritanceCatNumberParentCOPref: { 'en-us': - 'Enable Catalog Number Inheritance From Parent CO', + 'Enable {catalogNumber:string} Inheritance From Parent {collectionObject:string}', }, inheritanceCatNumberParentCOPrefDescription: { 'en-us': - 'Control whether component records inherit catalog numbers from their parent Collection Object.', + 'Control whether component records inherit the {catalogNumber:string} from the parent {collectionObject:string}.', }, uniqueCatNumberAcrossCompAndCo: { 'en-us': From f2c4c2b6a231bd38270729600999cb33219a344a Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:15:05 -0500 Subject: [PATCH 072/100] feat: add better icon, distinguish preferences --- specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx | 1 + .../js_src/lib/components/Header/userToolDefinitions.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx index ef689b4435e..793d9c6c750 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx @@ -107,6 +107,7 @@ export const icons = { minus: , minusCircle: , nonStrict: , + building: , pencil: , pencilAt: , photos: , diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index ef262b42e9f..7e423a86b32 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -53,14 +53,14 @@ const rawUserTools = ensure>>>()({ }, [preferencesText.customization()]: { userPreferences: { - title: preferencesText.preferences(), + title: preferencesText.userPreferences(), url: '/specify/user-preferences/', icon: icons.cog, }, collectionPreferences: { title: preferencesText.collectionPreferences(), url: '/specify/collection-preferences/', - icon: icons.collection, + icon: icons.building, enabled: () => hasToolPermission('resources', 'update'), }, schemaConfig: { From c08bc1347fc15f4760c5d631fcbe84587ef2ace2 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:20:43 -0500 Subject: [PATCH 073/100] fix: reorder trees in order of relevance --- .../Preferences/CollectionDefinitions.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 449b1ed2be8..32d63057f1f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -84,41 +84,41 @@ export const collectionPreferenceDefinitions = { title: treeText.synonymizedNodes(), description: treeText.synonymizedNodesDescription(), items: { - 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': + 'sp7.allow_adding_child_to_synonymized_parent.Taxon': definePref({ - title: () => tableLabel('GeologicTimePeriod'), + title: () => tableLabel('Taxon'), requiresReload: false, visible: true, defaultValue: false, type: 'java.lang.Boolean', }), - 'sp7.allow_adding_child_to_synonymized_parent.Taxon': + 'sp7.allow_adding_child_to_synonymized_parent.Geography': definePref({ - title: () => tableLabel('Taxon'), + title: () => tableLabel('Geography'), requiresReload: false, visible: true, defaultValue: false, type: 'java.lang.Boolean', }), - 'sp7.allow_adding_child_to_synonymized_parent.Geography': + 'sp7.allow_adding_child_to_synonymized_parent.Storage': definePref({ - title: () => tableLabel('Geography'), + title: () => tableLabel('Storage'), requiresReload: false, visible: true, defaultValue: false, type: 'java.lang.Boolean', }), - 'sp7.allow_adding_child_to_synonymized_parent.LithoStrat': + 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': definePref({ - title: () => tableLabel('LithoStrat'), + title: () => tableLabel('GeologicTimePeriod'), requiresReload: false, visible: true, defaultValue: false, type: 'java.lang.Boolean', }), - 'sp7.allow_adding_child_to_synonymized_parent.Storage': + 'sp7.allow_adding_child_to_synonymized_parent.LithoStrat': definePref({ - title: () => tableLabel('Storage'), + title: () => tableLabel('LithoStrat'), requiresReload: false, visible: true, defaultValue: false, From fc6616fa0b622ded0e371b45e6209b823a861c61 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:33:43 -0500 Subject: [PATCH 074/100] feat(collection preferences): add sidebar --- .../lib/components/Preferences/Aside.tsx | 5 +++- .../lib/components/Preferences/Editor.tsx | 24 ++++++++++++++++++- .../components/Preferences/useTopChild.tsx | 14 +++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx index 806575fafad..2e4e6cd813e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx @@ -7,18 +7,21 @@ import type { GetSet, WritableArray } from '../../utils/types'; import { Link } from '../Atoms/Link'; import { pathIsOverlay } from '../Router/UnloadProtect'; import { scrollIntoView } from '../TreeView/helpers'; +import type { PreferenceType } from './index'; import { usePrefDefinitions } from './index'; export function PreferencesAside({ activeCategory, setActiveCategory, references, + prefType = 'user', }: { readonly activeCategory: number | undefined; readonly setActiveCategory: (activeCategory: number | undefined) => void; readonly references: React.RefObject>; + readonly prefType?: PreferenceType; }): JSX.Element { - const definitions = usePrefDefinitions(); + const definitions = usePrefDefinitions(prefType); const navigate = useNavigate(); const location = useLocation(); const isInOverlay = pathIsOverlay(location.pathname); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 6b6be980014..7d29fcd96d9 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -4,7 +4,9 @@ import { useLiveState } from '../../hooks/useLiveState'; import type { AppResourceTabProps } from '../AppResources/TabDefinitions'; import type { PreferenceType } from '../Preferences'; import { PreferencesContent } from '../Preferences'; +import { PreferencesAside } from '../Preferences/Aside'; import { BasePreferences } from '../Preferences/BasePreferences'; +import { useTopChild } from '../Preferences/useTopChild'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; @@ -75,10 +77,30 @@ function createPreferencesEditor( const Provider = Context.Provider; const contentProps = prefType === undefined ? {} : { prefType }; + const { + visibleChild, + setVisibleChild, + references, + forwardRefs, + scrollContainerRef, + } = useTopChild(); + const asidePrefType = prefType ?? 'user'; return ( - +
+ + + +
); }; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx index cfa5e33cc54..ffd024d0195 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx @@ -54,10 +54,20 @@ export function useTopChild(): { const observer = new ResizeObserver(handleChange); observer.observe(container); - const scroll = listen(container, 'scroll', handleChange); + const scrollTargets: HTMLElement[] = []; + for ( + let element: HTMLElement | null = container; + element !== null; + element = element.parentElement + ) + scrollTargets.push(element); + const uniqueScrollTargets = Array.from(new Set(scrollTargets)); + const scrollCleanups = uniqueScrollTargets.map((target) => + listen(target, 'scroll', handleChange) + ); return (): void => { observer.disconnect(); - scroll(); + scrollCleanups.forEach((cleanup) => cleanup()); }; }, [container]); From 8f9682f59ec4edbcadfd57ec50d99318bb7da04b Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:43:59 -0500 Subject: [PATCH 075/100] feat(preferences): match user preferences visual --- .../lib/components/Preferences/index.tsx | 240 ++---------------- 1 file changed, 17 insertions(+), 223 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 02a26d1d4c7..ac4cd639ee6 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -14,30 +14,16 @@ import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; -import { AppResourceEditor } from '../AppResources/Editor'; -import { getScope, globalResourceKey } from '../AppResources/tree'; -import type { ScopedAppResourceDir } from '../AppResources/types'; import { Container, H2, Key } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; -import { DataEntry } from '../Atoms/DataEntry'; import { Form } from '../Atoms/Form'; import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; -import type { SerializedResource } from '../DataModel/helperTypes'; -import { fetchResource, strictIdFromUrl } from '../DataModel/resource'; -import { serializeResource } from '../DataModel/serializers'; -import type { - SpAppResource, - SpAppResourceDir, - SpViewSetObj, -} from '../DataModel/types'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission } from '../Permissions/helpers'; import { ProtectedTool } from '../Permissions/PermissionDenied'; -import { userTypes } from '../PickLists/definitions'; -import { formatUrl } from '../Router/queryString'; import { PreferencesAside } from './Aside'; import type { BasePreferences } from './BasePreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; @@ -113,20 +99,29 @@ const preferencesPromise = Promise.all([ collectionPreferences.fetch(), ]).then(f.true); -function Preferences(): JSX.Element { +function Preferences({ + prefType = 'user', +}: { + readonly prefType?: PreferenceType; +} = {}): JSX.Element { const [changesMade, handleChangesMade] = useBooleanState(); const [needsRestart, handleRestartNeeded] = useBooleanState(); const loading = React.useContext(LoadingContext); const navigate = useNavigate(); + const basePreferences = preferenceInstances[prefType]; + const heading = + prefType === 'collection' + ? preferencesText.collectionPreferences() + : preferencesText.preferences(); React.useEffect( () => - userPreferences.events.on('update', (payload) => { + basePreferences.events.on('update', (payload) => { if (payload?.definition?.requiresReload === true) handleRestartNeeded(); handleChangesMade(); }), - [handleChangesMade, handleRestartNeeded] + [basePreferences, handleChangesMade, handleRestartNeeded] ); const { @@ -139,12 +134,12 @@ function Preferences(): JSX.Element { return ( -

{preferencesText.preferences()}

+

{heading}

loading( - userPreferences + basePreferences .awaitSynced() .then(() => needsRestart @@ -160,10 +155,11 @@ function Preferences(): JSX.Element { > - +
@@ -540,7 +536,7 @@ function Item({ function CollectionPreferences(): JSX.Element { return ( - + ); } @@ -571,205 +567,3 @@ export function CollectionPreferencesWrapper(): JSX.Element | null { ); } - -type ResourceWithData = { - readonly id: number; - readonly data: string | null; - readonly name: string; - readonly mimetype: string | null; - readonly metadata: string | null; -}; - -type LoadedCollectionPreferences = { - readonly resource: SerializedResource; - readonly directory: ScopedAppResourceDir; - readonly data: ResourceWithData; -}; - -const isAppResource = ( - resource: SerializedResource -): resource is SerializedResource => - resource._tableName === 'SpAppResource'; - -function CollectionPreferencesStandalone(): JSX.Element { - const navigate = useNavigate(); - const [state, setState] = React.useState< - LoadedCollectionPreferences | undefined - >(undefined); - const [error, setError] = React.useState(undefined); - - const renderStatus = React.useCallback( - (body: React.ReactNode, role?: 'alert'): JSX.Element => ( - -

{preferencesText.collectionPreferences()}

-
- {body} -
-
- ), - [] - ); - - React.useEffect(() => { - let isMounted = true; - const load = async () => { - try { - const rawData = - (await collectionPreferences.fetch()) as ResourceWithData; - const data: ResourceWithData = { - ...rawData, - data: rawData.data ?? '', - }; - if (!isMounted) return; - const resource = await fetchResource('SpAppResource', data.id); - if (!isMounted) return; - const directory = await resolveDirectory(resource); - if (!isMounted) return; - setState({ resource, directory, data }); - } catch (loadError) { - if (!isMounted) return; - setError(loadError); - } - }; - load(); - return () => { - isMounted = false; - }; - }, []); - - const handleClone = React.useCallback( - ( - clonedResource: SerializedResource, - cloneId: number | undefined - ) => { - const appResourceClone = isAppResource(clonedResource) - ? clonedResource - : undefined; - if (appResourceClone === undefined) return; - const directoryKey = - state === undefined - ? globalResourceKey - : (getDirectoryKey(state.directory) ?? globalResourceKey); - navigate( - formatUrl('/specify/resources/app-resource/new/', { - directoryKey, - name: appResourceClone.name, - ...(appResourceClone.mimeType == null - ? {} - : { mimeType: appResourceClone.mimeType }), - clone: cloneId, - }) - ); - }, - [navigate, state] - ); - - if (error !== undefined && state === undefined) - return renderStatus( - 'Failed to open collection preferences. Try accessing them through App Resources.', - 'alert' - ); - - if (state === undefined) return renderStatus(commonText.loading()); - - return ( - - { - setState((previousState) => - previousState === undefined - ? previousState - : { - resource: - updatedResource as SerializedResource, - directory: updatedDirectory, - data: previousState.data, - } - ); - collectionPreferences - .fetch() - .then((rawData) => ({ - ...rawData, - data: rawData.data ?? '', - })) - .then((updatedData) => { - setState({ - resource: updatedResource as SerializedResource, - directory: updatedDirectory, - data: updatedData as ResourceWithData, - }); - }) - .catch((fetchError) => { - if (state === undefined) setError(fetchError); - }); - }} - > - {({ headerJsx, headerButtons, form, footer }): JSX.Element => ( - - - {headerJsx} - {headerButtons} - - {form} - {footer} - - )} - - - ); -} - -async function resolveDirectory( - resource: SerializedResource -): Promise { - const rawDirectory = resource.spAppResourceDir; - let directory: SerializedResource; - if (typeof rawDirectory === 'string') { - directory = await fetchResource( - 'SpAppResourceDir', - strictIdFromUrl(rawDirectory) - ); - } else if (typeof rawDirectory === 'object' && rawDirectory !== null) { - directory = serializeResource(rawDirectory); - } else { - throw new Error('Collection preferences resource is missing directory'); - } - return { - ...directory, - scope: getScope(directory), - }; -} - -function getDirectoryKey(directory: ScopedAppResourceDir): string | undefined { - if (directory.scope === 'global') return globalResourceKey; - if (directory.scope === 'discipline' && directory.discipline !== null) - return `discipline_${strictIdFromUrl(directory.discipline)}`; - if (directory.scope === 'collection' && directory.collection !== null) - return `collection_${strictIdFromUrl(directory.collection)}`; - if ( - directory.scope === 'userType' && - directory.collection !== null && - directory.userType !== null - ) { - const userTypeLabel = - userTypes.find( - (type) => type.toLowerCase() === directory.userType?.toLowerCase() - ) ?? directory.userType; - return `collection_${strictIdFromUrl(directory.collection)}_userType_${userTypeLabel}`; - } - if ( - directory.scope === 'user' && - directory.collection !== null && - directory.specifyUser !== null - ) - return `collection_${strictIdFromUrl(directory.collection)}_user_${strictIdFromUrl(directory.specifyUser)}`; - return undefined; -} From da3407e10976ad806ab51def32da9dd4f396f171 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:51:49 -0500 Subject: [PATCH 076/100] fix: failing test --- specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx | 2 +- .../frontend/js_src/lib/components/FormFields/Field.tsx | 6 +++--- .../js_src/lib/components/Header/userToolDefinitions.ts | 2 +- .../lib/components/Preferences/CollectionDefinitions.tsx | 2 -- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx index 793d9c6c750..b3dad736695 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx @@ -107,7 +107,7 @@ export const icons = { minus: , minusCircle: , nonStrict: , - building: , + office: , pencil: , pencilAt: , photos: , diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 1d1f69191a2..1fb8295b8dc 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -139,14 +139,14 @@ function Field({ // Check if collection pref wants to inherit primary cat num for empty CO cat num sibilings inside of a COG const [displayPrimaryCatNumberPref] = collectionPreferences.use( 'catalogNumberInheritance', - 'behavior', + 'collectionObject', 'inheritance' ); // Check if collection pref wants to inherit parent cat num for empty CO cat num children const [displayParentCatNumberPref] = collectionPreferences.use( - 'catalogNumberParentInheritance', - 'behavior', + 'catalogNumberInheritance', + 'component', 'inheritance' ); diff --git a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts index 7e423a86b32..3ed5ee2a4e3 100644 --- a/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Header/userToolDefinitions.ts @@ -60,7 +60,7 @@ const rawUserTools = ensure>>>()({ collectionPreferences: { title: preferencesText.collectionPreferences(), url: '/specify/collection-preferences/', - icon: icons.building, + icon: icons.office, enabled: () => hasToolPermission('resources', 'update'), }, schemaConfig: { diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 32d63057f1f..07c0756b394 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -260,8 +260,6 @@ export const collectionPreferenceDefinitions = { }, }, - /* catalogNumberParentInheritance removed - folded into catalogNumberInheritance */ - uniqueCatalogNumberAccrossComponentAndCO: { title: queryText.uniqueCatalogNumberAcrossComponentAndCo(), subCategories: { From 84720f7ed580a05870746b4ec2da51a6312eff42 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:07:50 -0500 Subject: [PATCH 077/100] fix: remove unused localization strings --- .../components/Preferences/CollectionDefinitions.tsx | 4 ++-- .../js_src/lib/localization/preferences.behavior.ts | 4 ---- specifyweb/frontend/js_src/lib/localization/query.ts | 10 ---------- specifyweb/frontend/js_src/lib/localization/tree.ts | 4 ---- 4 files changed, 2 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 07c0756b394..a6c56a06656 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -235,12 +235,12 @@ export const collectionPreferenceDefinitions = { items: { inheritance: definePref({ title: () => - preferencesText.inheritanceCatNumberPref({ + preferencesText.inheritanceCatNumberParentCOPref({ catalogNumber: getField( tables.CollectionObject, 'catalogNumber' ).label, - collectionObject: tables.CollectionObject.label, + collectionObject: tables.CollectionObject.label, }), description: () => preferencesText.inheritanceCatNumberParentCOPrefDescription({ diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 9b0cf0e6901..7c33aa8a54c 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -693,10 +693,6 @@ export const preferencesBehaviorDictionary = { 'en-us': 'If enabled, picklists of type "Entire Table" will only show items that are in use within the current collection.', }, - catalogNumberParentInheritanceDescription: { - 'en-us': - 'Control whether component records inherit catalog numbers from their parent Collection Object.', - }, useAccessibleFullDatePicker: { 'en-us': 'Use accessible full date picker', 'ru-ru': 'Используйте доступный полный выбор даты', diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index 0b711313afe..a591c9a7b89 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -955,16 +955,6 @@ export const queryText = createDictionary({ "ru-ru": "Наследование каталожного номера", "uk-ua": "Успадкування каталожних номерів", }, - catalogNumberParentCOInheritance: { - "en-us": "Catalog Number Parent Collection Object Inheritance", - "de-ch": "Katalognummer Übergeordnete Sammlung Objektvererbung", - "es-es": - "Herencia de objetos de la colección principal del número de catálogo", - "fr-fr": "Numéro de catalogue Collection parente Héritage d'objet", - "pt-br": "Herança de objeto de coleção pai de número de catálogo", - "ru-ru": "Номер каталога Родительская коллекция Объект Наследование", - "uk-ua": "Успадкування батьківського об'єкта колекції за номером каталогу", - }, uniqueCatalogNumberAcrossComponentAndCo: { "en-us": "Catalog Number Uniqueness Across Component And CO tables", "de-ch": "Eindeutigkeit der Katalognummer in Komponenten- und CO-Tabellen", diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 4f5bc571ab2..855a009bf51 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -713,8 +713,4 @@ export const treeText = createDictionary({ 'en-us': 'If enabled, this allows users to add children to synonymized parents and to synonymize a node with children.', }, - synonymizedNodesPermissionDescription: { - 'en-us': - 'Enable adding children to synonymized parents on this tree. When enabled, users may add child nodes beneath parents that have been marked as synonyms.', - }, } as const); From a0e546302ba2c795fa9e3dbe5fcbb21ff0049222 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:14:19 -0500 Subject: [PATCH 078/100] refactor: simplify code --- .../js_src/lib/components/Preferences/Editor.tsx | 4 ++-- .../lib/components/Preferences/useTopChild.tsx | 14 ++------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 7d29fcd96d9..18f1b272358 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -111,7 +111,7 @@ export const UserPreferencesEditor = createPreferencesEditor({ Context: userPreferences.Context, resourceName: 'UserPreferences', fetchUrl: '/context/user_resource/', - developmentGlobal: '_editingUserPreferences', + developmentGlobal: 'editingUserPreferences', dependencyResolver: ({ onChange }) => [onChange], }); @@ -120,7 +120,7 @@ export const CollectionPreferencesEditor = createPreferencesEditor({ Context: collectionPreferences.Context, resourceName: 'CollectionPreferences', fetchUrl: '/context/collection_resource/', - developmentGlobal: '_editingCollectionPreferences', + developmentGlobal: 'editingCollectionPreferences', prefType: 'collection', dependencyResolver: ({ data, onChange }) => [data, onChange], }); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx index ffd024d0195..cfa5e33cc54 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/useTopChild.tsx @@ -54,20 +54,10 @@ export function useTopChild(): { const observer = new ResizeObserver(handleChange); observer.observe(container); - const scrollTargets: HTMLElement[] = []; - for ( - let element: HTMLElement | null = container; - element !== null; - element = element.parentElement - ) - scrollTargets.push(element); - const uniqueScrollTargets = Array.from(new Set(scrollTargets)); - const scrollCleanups = uniqueScrollTargets.map((target) => - listen(target, 'scroll', handleChange) - ); + const scroll = listen(container, 'scroll', handleChange); return (): void => { observer.disconnect(); - scrollCleanups.forEach((cleanup) => cleanup()); + scroll(); }; }, [container]); From 7a18d71fe18fe0668fc58339205f5ea32c21a9a7 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:21:02 +0000 Subject: [PATCH 079/100] Lint code with ESLint and Prettier Triggered by a0e546302ba2c795fa9e3dbe5fcbb21ff0049222 on branch refs/heads/issue-7440 --- .../Preferences/CollectionDefinitions.tsx | 6 +- .../lib/components/Preferences/Editor.tsx | 2 +- .../lib/localization/preferences.behavior.ts | 2 +- .../frontend/js_src/lib/localization/query.ts | 1562 ++++++++--------- .../frontend/js_src/lib/localization/tree.ts | 1040 +++++------ 5 files changed, 1306 insertions(+), 1306 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index a6c56a06656..1e26542ee5c 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -14,13 +14,13 @@ import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure } from '../../utils/types'; import { camelToHuman } from '../../utils/utils'; -import type { StatLayout } from '../Statistics/types'; +import { getField } from '../DataModel/helpers'; import { genericTables } from '../DataModel/tables'; +import { tables } from '../DataModel/tables'; import type { Tables } from '../DataModel/types'; +import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; -import { tables } from '../DataModel/tables'; -import { getField } from '../DataModel/helpers'; const tableLabel = (tableName: keyof Tables): LocalizedString => genericTables[tableName]?.label ?? camelToHuman(tableName); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 18f1b272358..b7b53ff6ad1 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -6,9 +6,9 @@ import type { PreferenceType } from '../Preferences'; import { PreferencesContent } from '../Preferences'; import { PreferencesAside } from '../Preferences/Aside'; import { BasePreferences } from '../Preferences/BasePreferences'; -import { useTopChild } from '../Preferences/useTopChild'; import { userPreferenceDefinitions } from '../Preferences/UserDefinitions'; import { userPreferences } from '../Preferences/userPreferences'; +import { useTopChild } from '../Preferences/useTopChild'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; import type { GenericPreferences } from './types'; diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 7c33aa8a54c..0e0aef8ea43 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -729,7 +729,7 @@ export const preferencesBehaviorDictionary = { 'de-ch': 'Standardmäßig zu öffnender Datensatz', 'pt-br': 'Gravar para abrir por padrão', }, -altClickToSupressNewTab: { + altClickToSupressNewTab: { 'en-us': '{altKeyName:string}+Click to suppress new tab', 'ru-ru': diff --git a/specifyweb/frontend/js_src/lib/localization/query.ts b/specifyweb/frontend/js_src/lib/localization/query.ts index a591c9a7b89..618f5b01e38 100644 --- a/specifyweb/frontend/js_src/lib/localization/query.ts +++ b/specifyweb/frontend/js_src/lib/localization/query.ts @@ -4,968 +4,968 @@ * @module */ -import { createDictionary } from "./utils"; +import { createDictionary } from './utils'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file export const queryText = createDictionary({ query: { - "en-us": "Query", - "ru-ru": "Запрос", - "es-es": "Consulta", - "fr-fr": "Requête", - "uk-ua": "Запит", - "de-ch": "Abfrage", - "pt-br": "Consulta", + 'en-us': 'Query', + 'ru-ru': 'Запрос', + 'es-es': 'Consulta', + 'fr-fr': 'Requête', + 'uk-ua': 'Запит', + 'de-ch': 'Abfrage', + 'pt-br': 'Consulta', }, queries: { - "en-us": "Queries", - "ru-ru": "Запросы", - "es-es": "Consultas", - "fr-fr": "Requêtes", - "uk-ua": "Запити", - "de-ch": "Abfragen", - "pt-br": "Consultas", + 'en-us': 'Queries', + 'ru-ru': 'Запросы', + 'es-es': 'Consultas', + 'fr-fr': 'Requêtes', + 'uk-ua': 'Запити', + 'de-ch': 'Abfragen', + 'pt-br': 'Consultas', }, queryBuilder: { - "en-us": "Query Builder", - "ru-ru": "Конструктор запросов", - "es-es": "Generador de consultas", - "fr-fr": "Générateur de requêtes", - "uk-ua": "Конструктор запитів", - "de-ch": "Query Builder", - "pt-br": "Construtor de consultas", + 'en-us': 'Query Builder', + 'ru-ru': 'Конструктор запросов', + 'es-es': 'Generador de consultas', + 'fr-fr': 'Générateur de requêtes', + 'uk-ua': 'Конструктор запитів', + 'de-ch': 'Query Builder', + 'pt-br': 'Construtor de consultas', }, newQueryName: { - "en-us": "New Query", - "es-es": "Nueva consulta", - "uk-ua": "Новий запит", - "de-ch": "Neue Abfrage", - "fr-fr": "Nouvelle requête", - "ru-ru": "Новый запрос", - "pt-br": "Nova consulta", + 'en-us': 'New Query', + 'es-es': 'Nueva consulta', + 'uk-ua': 'Новий запит', + 'de-ch': 'Neue Abfrage', + 'fr-fr': 'Nouvelle requête', + 'ru-ru': 'Новый запрос', + 'pt-br': 'Nova consulta', }, searchFields: { comment: ` Used in a Query Combo Box's hover-over message to show which fields are being searched on `, - "en-us": "Searched fields", - "ru-ru": "Поля поиска", - "es-es": "Campos buscados", - "fr-fr": "Champs recherchés", - "uk-ua": "Пошукові поля", - "de-ch": "Durchsuchte Felder", - "pt-br": "Campos pesquisados", + 'en-us': 'Searched fields', + 'ru-ru': 'Поля поиска', + 'es-es': 'Campos buscados', + 'fr-fr': 'Champs recherchés', + 'uk-ua': 'Пошукові поля', + 'de-ch': 'Durchsuchte Felder', + 'pt-br': 'Campos pesquisados', }, any: { - "en-us": "Any", - "ru-ru": "Любой", - "es-es": "Cualquiera", - "fr-fr": "N'importe lequel", - "uk-ua": "Будь-який", - "de-ch": "Beliebig", - "pt-br": "Qualquer", + 'en-us': 'Any', + 'ru-ru': 'Любой', + 'es-es': 'Cualquiera', + 'fr-fr': "N'importe lequel", + 'uk-ua': 'Будь-який', + 'de-ch': 'Beliebig', + 'pt-br': 'Qualquer', }, startValue: { - "en-us": "Start Value", - "ru-ru": "Начальное значение", - "es-es": "Valor inicial", - "fr-fr": "Valeur de départ", - "uk-ua": "Початкове значення", - "de-ch": "Startwert", - "pt-br": "Valor inicial", + 'en-us': 'Start Value', + 'ru-ru': 'Начальное значение', + 'es-es': 'Valor inicial', + 'fr-fr': 'Valeur de départ', + 'uk-ua': 'Початкове значення', + 'de-ch': 'Startwert', + 'pt-br': 'Valor inicial', }, endValue: { - "en-us": "End Value", - "ru-ru": "Конечное значение", - "es-es": "Valor final", - "fr-fr": "Valeur finale", - "uk-ua": "Кінцеве значення", - "de-ch": "Endwert", - "pt-br": "Valor final", + 'en-us': 'End Value', + 'ru-ru': 'Конечное значение', + 'es-es': 'Valor final', + 'fr-fr': 'Valeur finale', + 'uk-ua': 'Кінцеве значення', + 'de-ch': 'Endwert', + 'pt-br': 'Valor final', }, saveQuery: { - "en-us": "Save Query", - "ru-ru": "Сохранить запрос", - "es-es": "Guardar consulta", - "fr-fr": "Enregistrer la requête", - "uk-ua": "Зберегти запит", - "de-ch": "Abfrage speichern", - "pt-br": "Salvar consulta", + 'en-us': 'Save Query', + 'ru-ru': 'Сохранить запрос', + 'es-es': 'Guardar consulta', + 'fr-fr': 'Enregistrer la requête', + 'uk-ua': 'Зберегти запит', + 'de-ch': 'Abfrage speichern', + 'pt-br': 'Salvar consulta', }, saveClonedQuery: { - "en-us": "Save query as...", - "ru-ru": "Сохранить запрос как...", - "es-es": "Guardar consulta como...", - "fr-fr": "Enregistrer la requête sous...", - "uk-ua": "Зберегти запит як...", - "de-ch": "Abfrage speichern unter...", - "pt-br": "Salvar consulta como...", + 'en-us': 'Save query as...', + 'ru-ru': 'Сохранить запрос как...', + 'es-es': 'Guardar consulta como...', + 'fr-fr': 'Enregistrer la requête sous...', + 'uk-ua': 'Зберегти запит як...', + 'de-ch': 'Abfrage speichern unter...', + 'pt-br': 'Salvar consulta como...', }, saveClonedQueryDescription: { - "en-us": - "The query will be saved with a new name leaving the current query unchanged.", - "ru-ru": - "Запрос будет сохранен под новым именем, текущий запрос останется без изменений.", - "es-es": - "La consulta se guardará con un nuevo nombre dejando la consulta actual sin cambios.", - "fr-fr": - "La requête sera enregistrée avec un nouveau nom, laissant la requête actuelle inchangée.", - "uk-ua": - "Запит буде збережено з новою назвою, а поточний запит залишиться без змін.", - "de-ch": - "Die Abfrage wird unter einem neuen Namen gespeichert, die aktuelle Abfrage bleibt unverändert.", - "pt-br": - "A consulta será salva com um novo nome, deixando a consulta atual inalterada.", + 'en-us': + 'The query will be saved with a new name leaving the current query unchanged.', + 'ru-ru': + 'Запрос будет сохранен под новым именем, текущий запрос останется без изменений.', + 'es-es': + 'La consulta se guardará con un nuevo nombre dejando la consulta actual sin cambios.', + 'fr-fr': + 'La requête sera enregistrée avec un nouveau nom, laissant la requête actuelle inchangée.', + 'uk-ua': + 'Запит буде збережено з новою назвою, а поточний запит залишиться без змін.', + 'de-ch': + 'Die Abfrage wird unter einem neuen Namen gespeichert, die aktuelle Abfrage bleibt unverändert.', + 'pt-br': + 'A consulta será salva com um novo nome, deixando a consulta atual inalterada.', }, queryDeleteIncomplete: { - "en-us": "Query definition contains incomplete fields", - "ru-ru": "Определение запроса содержит неполные поля", - "es-es": "La definición de consulta contiene campos incompletos", - "fr-fr": "La définition de la requête contient des champs incomplets", - "uk-ua": "Визначення запиту містить незаповнені поля", - "de-ch": "Abfragedefinition enthält unvollständige Felder", - "pt-br": "A definição da consulta contém campos incompletos", + 'en-us': 'Query definition contains incomplete fields', + 'ru-ru': 'Определение запроса содержит неполные поля', + 'es-es': 'La definición de consulta contiene campos incompletos', + 'fr-fr': 'La définition de la requête contient des champs incomplets', + 'uk-ua': 'Визначення запиту містить незаповнені поля', + 'de-ch': 'Abfragedefinition enthält unvollständige Felder', + 'pt-br': 'A definição da consulta contém campos incompletos', }, queryDeleteIncompleteDescription: { - "en-us": - "There are uncompleted fields in the query definition. Do you want to remove them?", - "ru-ru": - "В определении запроса есть незаполненные поля. Хотите их удалить?", - "es-es": - "Hay campos sin completar en la definición de la consulta. ¿Desea eliminarlos?", - "fr-fr": - "Il y a des champs incomplets dans la définition de la requête. Voulez-vous les supprimer ?", - "uk-ua": "У визначенні запиту є незаповнені поля. Ви хочете видалити їх?", - "de-ch": - "Die Abfragedefinition enthält unvollständige Felder. Möchten Sie diese entfernen?", - "pt-br": - "Há campos incompletos na definição da consulta. Deseja removê-los?", + 'en-us': + 'There are uncompleted fields in the query definition. Do you want to remove them?', + 'ru-ru': + 'В определении запроса есть незаполненные поля. Хотите их удалить?', + 'es-es': + 'Hay campos sin completar en la definición de la consulta. ¿Desea eliminarlos?', + 'fr-fr': + 'Il y a des champs incomplets dans la définition de la requête. Voulez-vous les supprimer ?', + 'uk-ua': 'У визначенні запиту є незаповнені поля. Ви хочете видалити їх?', + 'de-ch': + 'Die Abfragedefinition enthält unvollständige Felder. Möchten Sie diese entfernen?', + 'pt-br': + 'Há campos incompletos na definição da consulta. Deseja removê-los?', }, queryUnloadProtect: { - "en-us": "The new or modified query definition has not been saved", - "ru-ru": "Новое или измененное определение запроса не было сохранено.", - "es-es": "La definición de consulta nueva o modificada no se ha guardado", - "fr-fr": + 'en-us': 'The new or modified query definition has not been saved', + 'ru-ru': 'Новое или измененное определение запроса не было сохранено.', + 'es-es': 'La definición de consulta nueva o modificada no se ha guardado', + 'fr-fr': "La définition de requête nouvelle ou modifiée n'a pas été enregistrée", - "uk-ua": "Нове або змінене визначення запиту не було збережено", - "de-ch": - "Die neue oder geänderte Abfragedefinition wurde nicht gespeichert", - "pt-br": "A definição de consulta nova ou modificada não foi salva", + 'uk-ua': 'Нове або змінене визначення запиту не було збережено', + 'de-ch': + 'Die neue oder geänderte Abfragedefinition wurde nicht gespeichert', + 'pt-br': 'A definição de consulta nova ou modificada não foi salva', }, recordSetToQuery: { - comment: "Example: Creating a Record Set from Query", - "en-us": "Creating a {recordSetTable:string} from Query", - "ru-ru": "Создание {recordSetTable:string} из запроса", - "es-es": "Creando un {recordSetTable:string} a partir de una consulta", - "fr-fr": "Création d'un {recordSetTable:string} à partir d'une requête", - "uk-ua": "Створення {recordSetTable:string} із запиту", - "de-ch": "Erstellen eines {recordSetTable:string} aus einer Abfrage", - "pt-br": "Criando um {recordSetTable:string} a partir da consulta", + comment: 'Example: Creating a Record Set from Query', + 'en-us': 'Creating a {recordSetTable:string} from Query', + 'ru-ru': 'Создание {recordSetTable:string} из запроса', + 'es-es': 'Creando un {recordSetTable:string} a partir de una consulta', + 'fr-fr': "Création d'un {recordSetTable:string} à partir d'une requête", + 'uk-ua': 'Створення {recordSetTable:string} із запиту', + 'de-ch': 'Erstellen eines {recordSetTable:string} aus einer Abfrage', + 'pt-br': 'Criando um {recordSetTable:string} a partir da consulta', }, recordSetToQueryDescription: { - "en-us": "Generating {recordSetTable:string}...", - "ru-ru": "Генерация {recordSetTable:string}...", - "es-es": "Generando {recordSetTable:string}...", - "fr-fr": "Génération de {recordSetTable:string}...", - "uk-ua": "Створення {recordSetTable:string}...", - "de-ch": "{recordSetTable:string} wird generiert...", - "pt-br": "Gerando {recordSetTable:string}...", + 'en-us': 'Generating {recordSetTable:string}...', + 'ru-ru': 'Генерация {recordSetTable:string}...', + 'es-es': 'Generando {recordSetTable:string}...', + 'fr-fr': 'Génération de {recordSetTable:string}...', + 'uk-ua': 'Створення {recordSetTable:string}...', + 'de-ch': '{recordSetTable:string} wird generiert...', + 'pt-br': 'Gerando {recordSetTable:string}...', }, recordSetCreated: { - "en-us": "{recordSetTable:string} Created", - "ru-ru": "{recordSetTable:string} Создано", - "es-es": "{recordSetTable:string} Creado", - "fr-fr": "{recordSetTable:string} Créé", - "uk-ua": "{recordSetTable:string} Створено", - "de-ch": "{recordSetTable:string} Erstellt", - "pt-br": "{recordSetTable:string} Criado", + 'en-us': '{recordSetTable:string} Created', + 'ru-ru': '{recordSetTable:string} Создано', + 'es-es': '{recordSetTable:string} Creado', + 'fr-fr': '{recordSetTable:string} Créé', + 'uk-ua': '{recordSetTable:string} Створено', + 'de-ch': '{recordSetTable:string} Erstellt', + 'pt-br': '{recordSetTable:string} Criado', }, missingCoordinatesForKml: { - "en-us": "Unable to export to KML", - "ru-ru": "Невозможно экспортировать в KML", - "es-es": "No se puede exportar a KML", - "fr-fr": "Impossible d'exporter vers KML", - "uk-ua": "Не вдалося експортувати в KML", - "de-ch": "Export in KML nicht möglich", - "pt-br": "Não é possível exportar para KML", + 'en-us': 'Unable to export to KML', + 'ru-ru': 'Невозможно экспортировать в KML', + 'es-es': 'No se puede exportar a KML', + 'fr-fr': "Impossible d'exporter vers KML", + 'uk-ua': 'Не вдалося експортувати в KML', + 'de-ch': 'Export in KML nicht möglich', + 'pt-br': 'Não é possível exportar para KML', }, missingCoordinatesForKmlDescription: { - "en-us": "Please add latitude and longitude fields to the query.", - "ru-ru": "Пожалуйста, добавьте в запрос поля широты и долготы.", - "es-es": "Agregue campos de latitud y longitud a la consulta.", - "fr-fr": - "Veuillez ajouter les champs de latitude et de longitude à la requête.", - "uk-ua": "Будь ласка, додайте поля широти та довготи до запиту.", - "de-ch": "Bitte fügen Sie der Abfrage Breiten- und Längengradfelder hinzu.", - "pt-br": "Adicione campos de latitude e longitude à consulta.", + 'en-us': 'Please add latitude and longitude fields to the query.', + 'ru-ru': 'Пожалуйста, добавьте в запрос поля широты и долготы.', + 'es-es': 'Agregue campos de latitud y longitud a la consulta.', + 'fr-fr': + 'Veuillez ajouter les champs de latitude et de longitude à la requête.', + 'uk-ua': 'Будь ласка, додайте поля широти та довготи до запиту.', + 'de-ch': 'Bitte fügen Sie der Abfrage Breiten- und Längengradfelder hinzu.', + 'pt-br': 'Adicione campos de latitude e longitude à consulta.', }, queryExportStarted: { - "en-us": "Export File Being Created", - "ru-ru": "Создается экспортный файл", - "es-es": "Creando archivo de exportación", - "fr-fr": "Fichier d'exportation en cours de création", - "uk-ua": "Експортний файл створюється", - "de-ch": "Exportdatei wird erstellt", - "pt-br": "Arquivo de exportação sendo criado", + 'en-us': 'Export File Being Created', + 'ru-ru': 'Создается экспортный файл', + 'es-es': 'Creando archivo de exportación', + 'fr-fr': "Fichier d'exportation en cours de création", + 'uk-ua': 'Експортний файл створюється', + 'de-ch': 'Exportdatei wird erstellt', + 'pt-br': 'Arquivo de exportação sendo criado', }, queryExportStartedDescription: { - "en-us": - "A notification will appear when the export file is complete and ready for download.", - "es-es": - "Aparecerá una notificación cuando el archivo de exportación esté completo y listo para descargar.", - "uk-ua": - "Коли файл експорту буде завершено та готовий до завантаження, з’явиться сповіщення.", - "de-ch": - "Wenn die Exportdatei vollständig ist und zum Download bereit steht, wird eine Benachrichtigung angezeigt.", - "fr-fr": + 'en-us': + 'A notification will appear when the export file is complete and ready for download.', + 'es-es': + 'Aparecerá una notificación cuando el archivo de exportación esté completo y listo para descargar.', + 'uk-ua': + 'Коли файл експорту буде завершено та готовий до завантаження, з’явиться сповіщення.', + 'de-ch': + 'Wenn die Exportdatei vollständig ist und zum Download bereit steht, wird eine Benachrichtigung angezeigt.', + 'fr-fr': "Une notification apparaîtra lorsque le fichier d'exportation sera terminé et prêt à être téléchargé.", - "ru-ru": - "Когда файл экспорта будет завершен и готов к загрузке, появится уведомление.", - "pt-br": - "Uma notificação aparecerá quando o arquivo de exportação estiver concluído e pronto para download.", + 'ru-ru': + 'Когда файл экспорта будет завершен и готов к загрузке, появится уведомление.', + 'pt-br': + 'Uma notificação aparecerá quando o arquivo de exportação estiver concluído e pronto para download.', }, invalidPicklistValue: { - comment: "Used when selected pick list value is not one of allowed values", - "en-us": "{value:string} (current, invalid value)", - "ru-ru": "{value:string} (текущее, недопустимое значение)", - "es-es": "{value:string} (valor actual, no válido)", - "fr-fr": "{value:string} (valeur actuelle, non valide)", - "uk-ua": "{value:string} (поточне, недійсне значення)", - "de-ch": "{value:string} (aktueller, ungültiger Wert)", - "pt-br": "{value:string} (valor atual, inválido)", + comment: 'Used when selected pick list value is not one of allowed values', + 'en-us': '{value:string} (current, invalid value)', + 'ru-ru': '{value:string} (текущее, недопустимое значение)', + 'es-es': '{value:string} (valor actual, no válido)', + 'fr-fr': '{value:string} (valeur actuelle, non valide)', + 'uk-ua': '{value:string} (поточне, недійсне значення)', + 'de-ch': '{value:string} (aktueller, ungültiger Wert)', + 'pt-br': '{value:string} (valor atual, inválido)', }, queryRecordSetTitle: { - comment: "Used in query builder header when querying on record set", - "en-us": + comment: 'Used in query builder header when querying on record set', + 'en-us': 'Query: "{queryName:string}" on {recordSetTable:string}: "{recordSetName:string}"', - "ru-ru": + 'ru-ru': 'Запрос: "{queryName:string}" на {recordSetTable:string}: "{recordSetName:string}"', - "es-es": + 'es-es': 'Consulta: "{queryName:string}" en {recordSetTable:string}: "{recordSetName:string}"', - "fr-fr": - "Requête : « {queryName:string} » sur {recordSetTable:string} : « {recordSetName:string} »", - "uk-ua": + 'fr-fr': + 'Requête : « {queryName:string} » sur {recordSetTable:string} : « {recordSetName:string} »', + 'uk-ua': 'Запит: "{queryName:string}" на {recordSetTable:string}: "{recordSetName:string}"', - "de-ch": + 'de-ch': 'Abfrage: "{queryName:string}" auf {recordSetTable:string}: "{recordSetName:string}"', - "pt-br": + 'pt-br': 'Consulta: "{queryName:string}" em {recordSetTable:string}: "{recordSetName:string}"', }, treeQueryName: { - comment: "Used in query builder header when querying on tree node usages", - "en-us": '{tableName:string} using "{nodeFullName:string}"', - "ru-ru": "{tableName:string} используя «{nodeFullName:string}»", - "es-es": '{tableName:string} usando "{nodeFullName:string}"', - "fr-fr": "{tableName:string} en utilisant « {nodeFullName:string} »", - "uk-ua": '{tableName:string} за допомогою "{nodeFullName:string}"', - "de-ch": '{tableName:string} mit "{nodeFullName:string}"', - "pt-br": '{tableName:string} usando "{nodeFullName:string}"', + comment: 'Used in query builder header when querying on tree node usages', + 'en-us': '{tableName:string} using "{nodeFullName:string}"', + 'ru-ru': '{tableName:string} используя «{nodeFullName:string}»', + 'es-es': '{tableName:string} usando "{nodeFullName:string}"', + 'fr-fr': '{tableName:string} en utilisant « {nodeFullName:string} »', + 'uk-ua': '{tableName:string} за допомогою "{nodeFullName:string}"', + 'de-ch': '{tableName:string} mit "{nodeFullName:string}"', + 'pt-br': '{tableName:string} usando "{nodeFullName:string}"', }, newButtonDescription: { - "en-us": "Add New Field", - "ru-ru": "Добавить новое поле", - "es-es": "Agregar nuevo campo", - "fr-fr": "Ajouter un nouveau champ", - "uk-ua": "Додати нове поле", - "de-ch": "Neues Feld hinzufügen", - "pt-br": "Adicionar novo campo", + 'en-us': 'Add New Field', + 'ru-ru': 'Добавить новое поле', + 'es-es': 'Agregar nuevo campo', + 'fr-fr': 'Ajouter un nouveau champ', + 'uk-ua': 'Додати нове поле', + 'de-ch': 'Neues Feld hinzufügen', + 'pt-br': 'Adicionar novo campo', }, countOnly: { - comment: "Verb", - "en-us": "Count", - "ru-ru": "Считать", - "es-es": "Conteo", - "fr-fr": "Compter", - "uk-ua": "Рахувати", - "de-ch": "Zählen", - "pt-br": "Contar", + comment: 'Verb', + 'en-us': 'Count', + 'ru-ru': 'Считать', + 'es-es': 'Conteo', + 'fr-fr': 'Compter', + 'uk-ua': 'Рахувати', + 'de-ch': 'Zählen', + 'pt-br': 'Contar', }, distinct: { - "en-us": "Distinct", - "ru-ru": "Отчетливый", - "es-es": "Distinto", - "fr-fr": "Distinct", - "uk-ua": "Виразний", - "de-ch": "Unterscheidbar", - "pt-br": "Distinto", + 'en-us': 'Distinct', + 'ru-ru': 'Отчетливый', + 'es-es': 'Distinto', + 'fr-fr': 'Distinct', + 'uk-ua': 'Виразний', + 'de-ch': 'Unterscheidbar', + 'pt-br': 'Distinto', }, series: { - "en-us": "Series", - "de-ch": "Serie", - "es-es": "Serie", - "fr-fr": "Série", - "pt-br": "Série", - "ru-ru": "Ряд", - "uk-ua": "Серія", + 'en-us': 'Series', + 'de-ch': 'Serie', + 'es-es': 'Serie', + 'fr-fr': 'Série', + 'pt-br': 'Série', + 'ru-ru': 'Ряд', + 'uk-ua': 'Серія', }, createCsv: { - "en-us": "Create CSV", - "ru-ru": "Создать CSV-файл", - "es-es": "Crear CSV", - "fr-fr": "Créer un fichier CSV", - "uk-ua": "Створити CSV", - "de-ch": "CSV erstellen", - "pt-br": "Criar CSV", + 'en-us': 'Create CSV', + 'ru-ru': 'Создать CSV-файл', + 'es-es': 'Crear CSV', + 'fr-fr': 'Créer un fichier CSV', + 'uk-ua': 'Створити CSV', + 'de-ch': 'CSV erstellen', + 'pt-br': 'Criar CSV', }, createKml: { - "en-us": "Create KML", - "ru-ru": "Создать KML", - "es-es": "Crear KML", - "fr-fr": "Créer un fichier KML", - "uk-ua": "Створіть KML", - "de-ch": "KML erstellen", - "pt-br": "Criar KML", + 'en-us': 'Create KML', + 'ru-ru': 'Создать KML', + 'es-es': 'Crear KML', + 'fr-fr': 'Créer un fichier KML', + 'uk-ua': 'Створіть KML', + 'de-ch': 'KML erstellen', + 'pt-br': 'Criar KML', }, createRecordSet: { - "en-us": "Create {recordSetTable:string}", - "ru-ru": "Создать {recordSetTable:string}", - "es-es": "Crear {recordSetTable:string}", - "fr-fr": "Créer {recordSetTable:string}", - "uk-ua": "Створити {recordSetTable:string}", - "de-ch": "Erstellen {recordSetTable:string}", - "pt-br": "Criar {recordSetTable:string}", + 'en-us': 'Create {recordSetTable:string}', + 'ru-ru': 'Создать {recordSetTable:string}', + 'es-es': 'Crear {recordSetTable:string}', + 'fr-fr': 'Créer {recordSetTable:string}', + 'uk-ua': 'Створити {recordSetTable:string}', + 'de-ch': 'Erstellen {recordSetTable:string}', + 'pt-br': 'Criar {recordSetTable:string}', }, saveAs: { - "en-us": "Save As", - "es-es": "Guardar como", - "uk-ua": "Зберегти як", - "de-ch": "Speichern unter", - "fr-fr": "Enregistrer sous", - "ru-ru": "Сохранить как", - "pt-br": "Salvar como", + 'en-us': 'Save As', + 'es-es': 'Guardar como', + 'uk-ua': 'Зберегти як', + 'de-ch': 'Speichern unter', + 'fr-fr': 'Enregistrer sous', + 'ru-ru': 'Сохранить как', + 'pt-br': 'Salvar como', }, anyRank: { - "en-us": "(any rank)", - "ru-ru": "(любой ранг)", - "es-es": "(cualquier rango)", - "fr-fr": "(n'importe quel rang)", - "uk-ua": "(будь-який ранг)", - "de-ch": "(jeder Rang)", - "pt-br": "(qualquer classificação)", + 'en-us': '(any rank)', + 'ru-ru': '(любой ранг)', + 'es-es': '(cualquier rango)', + 'fr-fr': "(n'importe quel rang)", + 'uk-ua': '(будь-який ранг)', + 'de-ch': '(jeder Rang)', + 'pt-br': '(qualquer classificação)', }, anyTree: { - "en-us": "(any tree)", - "de-ch": "(jeder Baum)", - "es-es": "(cualquier árbol)", - "fr-fr": "(n'importe quel arbre)", - "pt-br": "(qualquer árvore)", - "ru-ru": "(любое дерево)", - "uk-ua": "(будь-яке дерево)", + 'en-us': '(any tree)', + 'de-ch': '(jeder Baum)', + 'es-es': '(cualquier árbol)', + 'fr-fr': "(n'importe quel arbre)", + 'pt-br': '(qualquer árvore)', + 'ru-ru': '(любое дерево)', + 'uk-ua': '(будь-яке дерево)', }, moveUp: { - comment: "As in move it up", - "en-us": "Move Up", - "ru-ru": "Двигаться вверх", - "es-es": "Mover hacia arriba", - "fr-fr": "Monter", - "uk-ua": "Рухатися вгору", - "de-ch": "Nach oben", - "pt-br": "Mover para cima", + comment: 'As in move it up', + 'en-us': 'Move Up', + 'ru-ru': 'Двигаться вверх', + 'es-es': 'Mover hacia arriba', + 'fr-fr': 'Monter', + 'uk-ua': 'Рухатися вгору', + 'de-ch': 'Nach oben', + 'pt-br': 'Mover para cima', }, moveDown: { - comment: "As in move it down", - "en-us": "Move Down", - "ru-ru": "Двигаться вниз", - "es-es": "Mover hacia abajo", - "fr-fr": "Descendre", - "uk-ua": "Рухатися вниз", - "de-ch": "Nach unten", - "pt-br": "Mover para baixo", + comment: 'As in move it down', + 'en-us': 'Move Down', + 'ru-ru': 'Двигаться вниз', + 'es-es': 'Mover hacia abajo', + 'fr-fr': 'Descendre', + 'uk-ua': 'Рухатися вниз', + 'de-ch': 'Nach unten', + 'pt-br': 'Mover para baixo', }, sort: { - "en-us": "Sort", - "ru-ru": "Сортировать", - "es-es": "Ordenar", - "fr-fr": "Trier", - "uk-ua": "Сортувати", - "de-ch": "Sortieren", - "pt-br": "Organizar", + 'en-us': 'Sort', + 'ru-ru': 'Сортировать', + 'es-es': 'Ordenar', + 'fr-fr': 'Trier', + 'uk-ua': 'Сортувати', + 'de-ch': 'Sortieren', + 'pt-br': 'Organizar', }, ascendingSort: { - "en-us": "Ascending Sort", - "ru-ru": "Сортировка по возрастанию", - "es-es": "Orden ascendente", - "fr-fr": "Tri croissant", - "uk-ua": "Сортування за зростанням", - "de-ch": "Aufsteigende Sortierung", - "pt-br": "Classificação crescente", + 'en-us': 'Ascending Sort', + 'ru-ru': 'Сортировка по возрастанию', + 'es-es': 'Orden ascendente', + 'fr-fr': 'Tri croissant', + 'uk-ua': 'Сортування за зростанням', + 'de-ch': 'Aufsteigende Sortierung', + 'pt-br': 'Classificação crescente', }, descendingSort: { - "en-us": "Descending Sort", - "ru-ru": "Сортировка по убыванию", - "es-es": "Orden descendente", - "fr-fr": "Tri décroissant", - "uk-ua": "Сортування за спаданням", - "de-ch": "Absteigende Sortierung", - "pt-br": "Classificação decrescente", + 'en-us': 'Descending Sort', + 'ru-ru': 'Сортировка по убыванию', + 'es-es': 'Orden descendente', + 'fr-fr': 'Tri décroissant', + 'uk-ua': 'Сортування за спаданням', + 'de-ch': 'Absteigende Sortierung', + 'pt-br': 'Classificação decrescente', }, negate: { - comment: "as in negate query condition", - "en-us": "Negate", - "ru-ru": "Отрицать", - "es-es": "Negar", - "fr-fr": "Nier", - "uk-ua": "Заперечувати", - "de-ch": "Negieren", - "pt-br": "Negar", + comment: 'as in negate query condition', + 'en-us': 'Negate', + 'ru-ru': 'Отрицать', + 'es-es': 'Negar', + 'fr-fr': 'Nier', + 'uk-ua': 'Заперечувати', + 'de-ch': 'Negieren', + 'pt-br': 'Negar', }, showButtonDescription: { - "en-us": "Show in results", - "es-es": "Mostrar en resultados", - "uk-ua": "Показати в результатах", - "de-ch": "In Ergebnissen anzeigen", - "fr-fr": "Afficher dans les résultats", - "ru-ru": "Показать в результатах", - "pt-br": "Mostrar nos resultados", + 'en-us': 'Show in results', + 'es-es': 'Mostrar en resultados', + 'uk-ua': 'Показати в результатах', + 'de-ch': 'In Ergebnissen anzeigen', + 'fr-fr': 'Afficher dans les résultats', + 'ru-ru': 'Показать в результатах', + 'pt-br': 'Mostrar nos resultados', }, aggregatedInline: { - "en-us": "(aggregated)", - "ru-ru": "(агрегированные)", - "es-es": "(agregado)", - "fr-fr": "(agrégés)", - "uk-ua": "(узагальнено)", - "de-ch": "(aggregiert)", - "pt-br": "(agregado)", + 'en-us': '(aggregated)', + 'ru-ru': '(агрегированные)', + 'es-es': '(agregado)', + 'fr-fr': '(agrégés)', + 'uk-ua': '(узагальнено)', + 'de-ch': '(aggregiert)', + 'pt-br': '(agregado)', }, formattedInline: { - "en-us": "(formatted)", - "ru-ru": "(отформатировано)", - "es-es": "(formateado)", - "fr-fr": "(formaté)", - "uk-ua": "(відформатований)", - "de-ch": "(formatiert)", - "pt-br": "(formatado)", + 'en-us': '(formatted)', + 'ru-ru': '(отформатировано)', + 'es-es': '(formateado)', + 'fr-fr': '(formaté)', + 'uk-ua': '(відформатований)', + 'de-ch': '(formatiert)', + 'pt-br': '(formatado)', }, like: { - "en-us": "Like", - "ru-ru": "Нравиться", - "es-es": "Como", - "fr-fr": "Comme", - "uk-ua": "Люблю", - "de-ch": "Wie", - "pt-br": "Como", + 'en-us': 'Like', + 'ru-ru': 'Нравиться', + 'es-es': 'Como', + 'fr-fr': 'Comme', + 'uk-ua': 'Люблю', + 'de-ch': 'Wie', + 'pt-br': 'Como', }, likeDescription: { comment: 'Explains the use of special symbols for the "like" query filter', - "en-us": + 'en-us': 'Use "%" to match any number of characters.\n\nUse "_" to match a single character', - "ru-ru": - "Используйте «%» для обозначения любого количества символов.\n\nИспользуйте «_» для обозначения одного символа.", - "es-es": + 'ru-ru': + 'Используйте «%» для обозначения любого количества символов.\n\nИспользуйте «_» для обозначения одного символа.', + 'es-es': 'Usar "%" para hacer coincidir cualquier número de caracteres.\n\nUsar "_" para hacer coincidir un solo carácter', - "fr-fr": + 'fr-fr': "Utilisez « % » pour correspondre à n'importe quel nombre de caractères.\n\nUtilisez « _ » pour correspondre à un seul caractère.", - "uk-ua": + 'uk-ua': 'Використовуйте "%", щоб відповідати будь-якій кількості символів.\n\nВикористовуйте "_", щоб відповідати одному символу', - "de-ch": - "Verwenden Sie „%“, um eine beliebige Anzahl von Zeichen abzugleichen.\n\nVerwenden Sie „_“, um ein einzelnes Zeichen abzugleichen", - "pt-br": + 'de-ch': + 'Verwenden Sie „%“, um eine beliebige Anzahl von Zeichen abzugleichen.\n\nVerwenden Sie „_“, um ein einzelnes Zeichen abzugleichen', + 'pt-br': 'Use "%" para corresponder a qualquer número de caracteres.\n\nUse "_" para corresponder a um único caractere.', }, equal: { - "en-us": "Equal", - "ru-ru": "Равный", - "es-es": "Igual", - "fr-fr": "Égal", - "uk-ua": "Рівні", - "de-ch": "Gleich", - "pt-br": "Igual", + 'en-us': 'Equal', + 'ru-ru': 'Равный', + 'es-es': 'Igual', + 'fr-fr': 'Égal', + 'uk-ua': 'Рівні', + 'de-ch': 'Gleich', + 'pt-br': 'Igual', }, greaterThan: { - "en-us": "Greater than", - "ru-ru": "Больше чем", - "es-es": "Mayor que", - "fr-fr": "Plus grand que", - "uk-ua": "Більш чим", - "de-ch": "Größer als", - "pt-br": "Maior que", + 'en-us': 'Greater than', + 'ru-ru': 'Больше чем', + 'es-es': 'Mayor que', + 'fr-fr': 'Plus grand que', + 'uk-ua': 'Більш чим', + 'de-ch': 'Größer als', + 'pt-br': 'Maior que', }, lessThan: { - "en-us": "Less than", - "ru-ru": "Меньше, чем", - "es-es": "Menor que", - "fr-fr": "Moins que", - "uk-ua": "Менше ніж", - "de-ch": "Weniger als", - "pt-br": "Menor que", + 'en-us': 'Less than', + 'ru-ru': 'Меньше, чем', + 'es-es': 'Menor que', + 'fr-fr': 'Moins que', + 'uk-ua': 'Менше ніж', + 'de-ch': 'Weniger als', + 'pt-br': 'Menor que', }, greaterOrEqualTo: { - "en-us": "Greater or Equal to", - "ru-ru": "Больше или равно", - "es-es": "Mayor o igual a", - "fr-fr": "Supérieur ou égal à", - "uk-ua": "Більше або дорівнює", - "de-ch": "Größer oder gleich", - "pt-br": "Maior ou igual a", + 'en-us': 'Greater or Equal to', + 'ru-ru': 'Больше или равно', + 'es-es': 'Mayor o igual a', + 'fr-fr': 'Supérieur ou égal à', + 'uk-ua': 'Більше або дорівнює', + 'de-ch': 'Größer oder gleich', + 'pt-br': 'Maior ou igual a', }, lessOrEqualTo: { - "en-us": "Less or Equal to", - "ru-ru": "Меньше или равно", - "es-es": "Menor o igual a", - "fr-fr": "Inférieur ou égal à", - "uk-ua": "Менше або дорівнює", - "de-ch": "Kleiner oder gleich", - "pt-br": "Menor ou igual a", + 'en-us': 'Less or Equal to', + 'ru-ru': 'Меньше или равно', + 'es-es': 'Menor o igual a', + 'fr-fr': 'Inférieur ou égal à', + 'uk-ua': 'Менше або дорівнює', + 'de-ch': 'Kleiner oder gleich', + 'pt-br': 'Menor ou igual a', }, true: { - "en-us": "True", - "ru-ru": "Истинный", - "es-es": "Verdadero", - "fr-fr": "Vrai", - "uk-ua": "правда", - "de-ch": "WAHR", - "pt-br": "Verdadeiro", + 'en-us': 'True', + 'ru-ru': 'Истинный', + 'es-es': 'Verdadero', + 'fr-fr': 'Vrai', + 'uk-ua': 'правда', + 'de-ch': 'WAHR', + 'pt-br': 'Verdadeiro', }, false: { - "en-us": "False", - "ru-ru": "ЛОЖЬ", - "es-es": "Falso", - "fr-fr": "FAUX", - "uk-ua": "помилковий", - "de-ch": "FALSCH", - "pt-br": "Falso", + 'en-us': 'False', + 'ru-ru': 'ЛОЖЬ', + 'es-es': 'Falso', + 'fr-fr': 'FAUX', + 'uk-ua': 'помилковий', + 'de-ch': 'FALSCH', + 'pt-br': 'Falso', }, trueOrNull: { - "en-us": "True or Empty", - "ru-ru": "Истина или Пусто", - "es-es": "Verdadero o vacío", - "fr-fr": "Vrai ou vide", - "uk-ua": "True або Empty", - "de-ch": "Wahr oder leer", - "pt-br": "Verdadeiro ou Vazio", + 'en-us': 'True or Empty', + 'ru-ru': 'Истина или Пусто', + 'es-es': 'Verdadero o vacío', + 'fr-fr': 'Vrai ou vide', + 'uk-ua': 'True або Empty', + 'de-ch': 'Wahr oder leer', + 'pt-br': 'Verdadeiro ou Vazio', }, falseOrNull: { - "en-us": "False or Empty", - "ru-ru": "Ложь или пусто", - "es-es": "Falso o vacío", - "fr-fr": "Faux ou vide", - "uk-ua": "False або Empty", - "de-ch": "Falsch oder leer", - "pt-br": "Falso ou Vazio", + 'en-us': 'False or Empty', + 'ru-ru': 'Ложь или пусто', + 'es-es': 'Falso o vacío', + 'fr-fr': 'Faux ou vide', + 'uk-ua': 'False або Empty', + 'de-ch': 'Falsch oder leer', + 'pt-br': 'Falso ou Vazio', }, between: { - "en-us": "Between", - "ru-ru": "Между", - "es-es": "Entre", - "fr-fr": "Entre", - "uk-ua": "Між", - "de-ch": "Zwischen", - "pt-br": "Entre", + 'en-us': 'Between', + 'ru-ru': 'Между', + 'es-es': 'Entre', + 'fr-fr': 'Entre', + 'uk-ua': 'Між', + 'de-ch': 'Zwischen', + 'pt-br': 'Entre', }, in: { - "en-us": "In", - "ru-ru": "В", - "es-es": "En", - "fr-fr": "Dans", - "uk-ua": "в", - "de-ch": "In", - "pt-br": "Em", + 'en-us': 'In', + 'ru-ru': 'В', + 'es-es': 'En', + 'fr-fr': 'Dans', + 'uk-ua': 'в', + 'de-ch': 'In', + 'pt-br': 'Em', }, inDescription: { - "en-us": "A comma-separated list of values", - "ru-ru": "Список значений, разделенных запятыми", - "es-es": "Una lista de valores separados por comas", - "fr-fr": "Une liste de valeurs séparées par des virgules", - "uk-ua": "Список значень, розділених комами", - "de-ch": "Eine durch Kommas getrennte Liste von Werten", - "pt-br": "Uma lista de valores separados por vírgulas", + 'en-us': 'A comma-separated list of values', + 'ru-ru': 'Список значений, разделенных запятыми', + 'es-es': 'Una lista de valores separados por comas', + 'fr-fr': 'Une liste de valeurs séparées par des virgules', + 'uk-ua': 'Список значень, розділених комами', + 'de-ch': 'Eine durch Kommas getrennte Liste von Werten', + 'pt-br': 'Uma lista de valores separados por vírgulas', }, contains: { - "en-us": "Contains", - "ru-ru": "Содержит", - "es-es": "Contiene", - "fr-fr": "Contient", - "uk-ua": "Містить", - "de-ch": "Enthält", - "pt-br": "Contém", + 'en-us': 'Contains', + 'ru-ru': 'Содержит', + 'es-es': 'Contiene', + 'fr-fr': 'Contient', + 'uk-ua': 'Містить', + 'de-ch': 'Enthält', + 'pt-br': 'Contém', }, empty: { - "en-us": "Empty", - "ru-ru": "Пустой", - "es-es": "Vacío", - "fr-fr": "Vide", - "uk-ua": "Порожній", - "de-ch": "Leer", - "pt-br": "Vazio", + 'en-us': 'Empty', + 'ru-ru': 'Пустой', + 'es-es': 'Vacío', + 'fr-fr': 'Vide', + 'uk-ua': 'Порожній', + 'de-ch': 'Leer', + 'pt-br': 'Vazio', }, and: { - "en-us": "and", - "ru-ru": "и", - "es-es": "y", - "fr-fr": "et", - "uk-ua": "і", - "de-ch": "Und", - "pt-br": "e", + 'en-us': 'and', + 'ru-ru': 'и', + 'es-es': 'y', + 'fr-fr': 'et', + 'uk-ua': 'і', + 'de-ch': 'Und', + 'pt-br': 'e', }, startsWith: { - "en-us": "Starts With", - "ru-ru": "Начинается с", - "es-es": "Comienza con", - "fr-fr": "Commence par", - "uk-ua": "Починається з", - "de-ch": "Beginnt mit", - "pt-br": "Começa com", + 'en-us': 'Starts With', + 'ru-ru': 'Начинается с', + 'es-es': 'Comienza con', + 'fr-fr': 'Commence par', + 'uk-ua': 'Починається з', + 'de-ch': 'Beginnt mit', + 'pt-br': 'Começa com', }, endsWith: { - "en-us": "Ends With", - "de-ch": "Endet mit", - "es-es": "Termina con", - "fr-fr": "Se termine par", - "pt-br": "Termina com", - "ru-ru": "Заканчивается с", - "uk-ua": "Закінчується на", + 'en-us': 'Ends With', + 'de-ch': 'Endet mit', + 'es-es': 'Termina con', + 'fr-fr': 'Se termine par', + 'pt-br': 'Termina com', + 'ru-ru': 'Заканчивается с', + 'uk-ua': 'Закінчується на', }, or: { - "en-us": "or", - "ru-ru": "или", - "es-es": "o", - "fr-fr": "ou", - "uk-ua": "або", - "de-ch": "oder", - "pt-br": "ou", + 'en-us': 'or', + 'ru-ru': 'или', + 'es-es': 'o', + 'fr-fr': 'ou', + 'uk-ua': 'або', + 'de-ch': 'oder', + 'pt-br': 'ou', }, yes: { - "en-us": "Yes", - "ru-ru": "Да", - "es-es": "Sí", - "fr-fr": "Oui", - "uk-ua": "Так", - "de-ch": "Ja", - "pt-br": "Sim", + 'en-us': 'Yes', + 'ru-ru': 'Да', + 'es-es': 'Sí', + 'fr-fr': 'Oui', + 'uk-ua': 'Так', + 'de-ch': 'Ja', + 'pt-br': 'Sim', }, queryResults: { - "en-us": "Query Results", - "ru-ru": "Результаты запроса", - "es-es": "Resultados de la consulta", - "fr-fr": "Résultats de la requête", - "uk-ua": "Результати запиту", - "de-ch": "Abfrageergebnisse", - "pt-br": "Resultados da consulta", + 'en-us': 'Query Results', + 'ru-ru': 'Результаты запроса', + 'es-es': 'Resultados de la consulta', + 'fr-fr': 'Résultats de la requête', + 'uk-ua': 'Результати запиту', + 'de-ch': 'Abfrageergebnisse', + 'pt-br': 'Resultados da consulta', }, browseInForms: { - "en-us": "Browse in Forms", - "ru-ru": "Просмотр в формах", - "es-es": "Navegar en formularios", - "fr-fr": "Parcourir les formulaires", - "uk-ua": "Перегляд у Формах", - "de-ch": "In Formularen blättern", - "pt-br": "Navegar em Formulários", + 'en-us': 'Browse in Forms', + 'ru-ru': 'Просмотр в формах', + 'es-es': 'Navegar en formularios', + 'fr-fr': 'Parcourir les formulaires', + 'uk-ua': 'Перегляд у Формах', + 'de-ch': 'In Formularen blättern', + 'pt-br': 'Navegar em Formulários', }, configureQueryTables: { - "en-us": "Configure visible query tables", - "ru-ru": "Настроить видимые таблицы запросов", - "es-es": "Configurar tablas de consulta visibles", - "fr-fr": "Configurer les tables de requête visibles", - "uk-ua": "Налаштувати видимі таблиці запитів", - "de-ch": "Konfigurieren sichtbarer Abfragetabellen", - "pt-br": "Configurar tabelas de consulta visíveis", + 'en-us': 'Configure visible query tables', + 'ru-ru': 'Настроить видимые таблицы запросов', + 'es-es': 'Configurar tablas de consulta visibles', + 'fr-fr': 'Configurer les tables de requête visibles', + 'uk-ua': 'Налаштувати видимі таблиці запитів', + 'de-ch': 'Konfigurieren sichtbarer Abfragetabellen', + 'pt-br': 'Configurar tabelas de consulta visíveis', }, exportQueryForDwca: { - "en-us": "Export query for DwCA definition", - "ru-ru": "Экспорт запроса на определение DwCA", - "es-es": "Consulta de exportación para la definición de DwCA", - "fr-fr": "Requête d'exportation pour la définition DwCA", - "uk-ua": "Експорт запиту для визначення DwCA", - "de-ch": "Exportabfrage für DwCA-Definition", - "pt-br": "Consulta de exportação para definição DwCA", + 'en-us': 'Export query for DwCA definition', + 'ru-ru': 'Экспорт запроса на определение DwCA', + 'es-es': 'Consulta de exportación para la definición de DwCA', + 'fr-fr': "Requête d'exportation pour la définition DwCA", + 'uk-ua': 'Експорт запиту для визначення DwCA', + 'de-ch': 'Exportabfrage für DwCA-Definition', + 'pt-br': 'Consulta de exportação para definição DwCA', }, exportQueryAsReport: { - "en-us": "Define report based on query", - "ru-ru": "Определить отчет на основе запроса", - "es-es": "Definir informe basado en consulta", - "fr-fr": "Définir un rapport basé sur une requête", - "uk-ua": "Визначити звіт на основі запиту", - "de-ch": "Definieren Sie den Bericht basierend auf der Abfrage", - "pt-br": "Definir relatório com base na consulta", + 'en-us': 'Define report based on query', + 'ru-ru': 'Определить отчет на основе запроса', + 'es-es': 'Definir informe basado en consulta', + 'fr-fr': 'Définir un rapport basé sur une requête', + 'uk-ua': 'Визначити звіт на основі запиту', + 'de-ch': 'Definieren Sie den Bericht basierend auf der Abfrage', + 'pt-br': 'Definir relatório com base na consulta', }, exportQueryAsLabel: { - "en-us": "Define label based on query", - "ru-ru": "Определить метку на основе запроса", - "es-es": "Definir etiqueta según consulta", - "fr-fr": "Définir l'étiquette en fonction de la requête", - "uk-ua": "Визначте мітку на основі запиту", - "de-ch": "Definieren Sie das Label basierend auf der Abfrage", - "pt-br": "Definir rótulo com base na consulta", + 'en-us': 'Define label based on query', + 'ru-ru': 'Определить метку на основе запроса', + 'es-es': 'Definir etiqueta según consulta', + 'fr-fr': "Définir l'étiquette en fonction de la requête", + 'uk-ua': 'Визначте мітку на основі запиту', + 'de-ch': 'Definieren Sie das Label basierend auf der Abfrage', + 'pt-br': 'Definir rótulo com base na consulta', }, treeMerge: { - comment: "Audit Log Action Type", - "en-us": "Tree Merge", - "ru-ru": "Слияние деревьев", - "es-es": "Fusión de árboles", - "fr-fr": "Fusion d'arbres", - "uk-ua": "Об'єднання дерев", - "de-ch": "Baumzusammenführung", - "pt-br": "Mesclagem de Árvores", + comment: 'Audit Log Action Type', + 'en-us': 'Tree Merge', + 'ru-ru': 'Слияние деревьев', + 'es-es': 'Fusión de árboles', + 'fr-fr': "Fusion d'arbres", + 'uk-ua': "Об'єднання дерев", + 'de-ch': 'Baumzusammenführung', + 'pt-br': 'Mesclagem de Árvores', }, treeMove: { - comment: "Audit Log Action Type", - "en-us": "Tree Move", - "ru-ru": "Перемещение дерева", - "es-es": "Movimiento de árbol", - "fr-fr": "Déplacement d'arbre", - "uk-ua": "Переміщення дерева", - "de-ch": "Baum verschieben", - "pt-br": "Movimentação de árvores", + comment: 'Audit Log Action Type', + 'en-us': 'Tree Move', + 'ru-ru': 'Перемещение дерева', + 'es-es': 'Movimiento de árbol', + 'fr-fr': "Déplacement d'arbre", + 'uk-ua': 'Переміщення дерева', + 'de-ch': 'Baum verschieben', + 'pt-br': 'Movimentação de árvores', }, treeSynonymize: { - comment: "Audit Log Action Type", - "en-us": "Tree Synonymize", - "ru-ru": "Дерево Синонимизировать", - "es-es": "Árbol Sinónimos", - "fr-fr": "Synonyme d'arbre", - "uk-ua": "Синонімізувати дерево", - "de-ch": "Baum synonymisieren", - "pt-br": "Árvore Sinonímia", + comment: 'Audit Log Action Type', + 'en-us': 'Tree Synonymize', + 'ru-ru': 'Дерево Синонимизировать', + 'es-es': 'Árbol Sinónimos', + 'fr-fr': "Synonyme d'arbre", + 'uk-ua': 'Синонімізувати дерево', + 'de-ch': 'Baum synonymisieren', + 'pt-br': 'Árvore Sinonímia', }, treeDesynonymize: { - comment: "Audit Log Action Type", - "en-us": "Tree Desynonymize", - "ru-ru": "Десинонимизация дерева", - "es-es": "Desinonimizar árboles", - "fr-fr": "Arbre désynonymisé", - "uk-ua": "Десинонімізація дерева", - "de-ch": "Baum desynonymisieren", - "pt-br": "Árvore Dessinonimizar", + comment: 'Audit Log Action Type', + 'en-us': 'Tree Desynonymize', + 'ru-ru': 'Десинонимизация дерева', + 'es-es': 'Desinonimizar árboles', + 'fr-fr': 'Arbre désynonymisé', + 'uk-ua': 'Десинонімізація дерева', + 'de-ch': 'Baum desynonymisieren', + 'pt-br': 'Árvore Dessinonimizar', }, treeBulkMove: { - comment: "Audit Log Action Type", - "en-us": "Tree Bulk Move", - "de-ch": "Massenbewegung von Bäumen", - "es-es": "Movimiento masivo de árboles", - "fr-fr": "Déplacement d'arbres en vrac", - "ru-ru": "Массовая перевозка деревьев", - "uk-ua": "Масове переміщення дерева", - "pt-br": "Mudança de árvores em massa", + comment: 'Audit Log Action Type', + 'en-us': 'Tree Bulk Move', + 'de-ch': 'Massenbewegung von Bäumen', + 'es-es': 'Movimiento masivo de árboles', + 'fr-fr': "Déplacement d'arbres en vrac", + 'ru-ru': 'Массовая перевозка деревьев', + 'uk-ua': 'Масове переміщення дерева', + 'pt-br': 'Mudança de árvores em massa', }, tooLongErrorMessage: { - "en-us": - "Field value is too long. Max allowed length is {maxLength:number|formatted}", - "ru-ru": - "Значение поля слишком длинное. Максимально допустимая длина: {maxLength:number|formatted}.", - "es-es": - "El valor del campo es demasiado largo. La longitud máxima permitida es {maxLength:number|formatted}.", - "fr-fr": - "La valeur du champ est trop longue. La longueur maximale autorisée est {maxLength:number|formatted}.", - "uk-ua": - "Значення поля задовге. Максимальна дозволена довжина {maxLength:number|formatted}", - "de-ch": - "Der Feldwert ist zu lang. Die maximal zulässige Länge beträgt {maxLength:number|formatted}", - "pt-br": - "O valor do campo é muito longo. O comprimento máximo permitido é {maxLength:number|formatted}", + 'en-us': + 'Field value is too long. Max allowed length is {maxLength:number|formatted}', + 'ru-ru': + 'Значение поля слишком длинное. Максимально допустимая длина: {maxLength:number|formatted}.', + 'es-es': + 'El valor del campo es demasiado largo. La longitud máxima permitida es {maxLength:number|formatted}.', + 'fr-fr': + 'La valeur du champ est trop longue. La longueur maximale autorisée est {maxLength:number|formatted}.', + 'uk-ua': + 'Значення поля задовге. Максимальна дозволена довжина {maxLength:number|formatted}', + 'de-ch': + 'Der Feldwert ist zu lang. Die maximal zulässige Länge beträgt {maxLength:number|formatted}', + 'pt-br': + 'O valor do campo é muito longo. O comprimento máximo permitido é {maxLength:number|formatted}', }, future: { - "en-us": "in the future", - "de-ch": "in der Zukunft", - "es-es": "en el futuro", - "fr-fr": "à l'avenir", - "ru-ru": "в будущем", - "uk-ua": "в майбутньому", - "pt-br": "no futuro", + 'en-us': 'in the future', + 'de-ch': 'in der Zukunft', + 'es-es': 'en el futuro', + 'fr-fr': "à l'avenir", + 'ru-ru': 'в будущем', + 'uk-ua': 'в майбутньому', + 'pt-br': 'no futuro', }, past: { - "en-us": "in the past", - "de-ch": "in der Vergangenheit", - "es-es": "en el pasado", - "fr-fr": "dans le passé", - "ru-ru": "в прошлом", - "uk-ua": "в минулому", - "pt-br": "no passado", + 'en-us': 'in the past', + 'de-ch': 'in der Vergangenheit', + 'es-es': 'en el pasado', + 'fr-fr': 'dans le passé', + 'ru-ru': 'в прошлом', + 'uk-ua': 'в минулому', + 'pt-br': 'no passado', }, days: { - "en-us": "Days", - "es-es": "Días", - "fr-fr": "Jours", - "ru-ru": "Дни", - "uk-ua": "днів", - "de-ch": "Tage", - "pt-br": "Dias", + 'en-us': 'Days', + 'es-es': 'Días', + 'fr-fr': 'Jours', + 'ru-ru': 'Дни', + 'uk-ua': 'днів', + 'de-ch': 'Tage', + 'pt-br': 'Dias', }, weeks: { - "en-us": "Weeks", - "de-ch": "Wochen", - "es-es": "Semanas", - "fr-fr": "Semaines", - "ru-ru": "Недели", - "uk-ua": "тижнів", - "pt-br": "Semanas", + 'en-us': 'Weeks', + 'de-ch': 'Wochen', + 'es-es': 'Semanas', + 'fr-fr': 'Semaines', + 'ru-ru': 'Недели', + 'uk-ua': 'тижнів', + 'pt-br': 'Semanas', }, months: { - "en-us": "Months", - "de-ch": "Monate", - "es-es": "Meses", - "fr-fr": "Mois", - "ru-ru": "Месяцы", - "uk-ua": "Місяці", - "pt-br": "Meses", + 'en-us': 'Months', + 'de-ch': 'Monate', + 'es-es': 'Meses', + 'fr-fr': 'Mois', + 'ru-ru': 'Месяцы', + 'uk-ua': 'Місяці', + 'pt-br': 'Meses', }, years: { - "en-us": "Years", - "de-ch": "Jahre", - "es-es": "Años", - "fr-fr": "Années", - "ru-ru": "Годы", - "uk-ua": "років", - "pt-br": "Anos", + 'en-us': 'Years', + 'de-ch': 'Jahre', + 'es-es': 'Años', + 'fr-fr': 'Années', + 'ru-ru': 'Годы', + 'uk-ua': 'років', + 'pt-br': 'Anos', }, year: { - "en-us": "Year", - "de-ch": "Jahr", - "es-es": "Año", - "fr-fr": "Année", - "ru-ru": "Год", - "uk-ua": "рік", - "pt-br": "", + 'en-us': 'Year', + 'de-ch': 'Jahr', + 'es-es': 'Año', + 'fr-fr': 'Année', + 'ru-ru': 'Год', + 'uk-ua': 'рік', + 'pt-br': '', }, relativeDate: { comment: ` Used in query builder lines, will be shown as a number followed by a period of time (ie: day, month or week) then a direction (past or future) `, - "en-us": - "{size:number} {type:string} {direction:string}", - "de-ch": - "{size:number} {type:string} {direction:string}", - "es-es": - "{size:number} {type:string} {direction:string}", - "fr-fr": - "{size:number} {type:string} {direction:string}", - "ru-ru": - "{size:number} {type:string} {direction:string}", - "uk-ua": - "{size:number} {type:string} {direction:string}", - "pt-br": - "{size:number} {type:string} {direction:string}", + 'en-us': + '{size:number} {type:string} {direction:string}', + 'de-ch': + '{size:number} {type:string} {direction:string}', + 'es-es': + '{size:number} {type:string} {direction:string}', + 'fr-fr': + '{size:number} {type:string} {direction:string}', + 'ru-ru': + '{size:number} {type:string} {direction:string}', + 'uk-ua': + '{size:number} {type:string} {direction:string}', + 'pt-br': + '{size:number} {type:string} {direction:string}', }, importHiddenFields: { - "en-us": "The following fields are hidden in the query you imported:", - "es-es": "Los siguientes campos están ocultos en la consulta que importó:", - "fr-fr": - "Les champs suivants sont masqués dans la requête que vous avez importée :", - "ru-ru": "В импортированном вами запросе скрыты следующие поля:", - "uk-ua": "В імпортованому вами запиті приховано такі поля:", - "de-ch": - "Die folgenden Felder sind in der von Ihnen importierten Abfrage ausgeblendet:", - "pt-br": "Os seguintes campos estão ocultos na consulta que você importou:", + 'en-us': 'The following fields are hidden in the query you imported:', + 'es-es': 'Los siguientes campos están ocultos en la consulta que importó:', + 'fr-fr': + 'Les champs suivants sont masqués dans la requête que vous avez importée :', + 'ru-ru': 'В импортированном вами запросе скрыты следующие поля:', + 'uk-ua': 'В імпортованому вами запиті приховано такі поля:', + 'de-ch': + 'Die folgenden Felder sind in der von Ihnen importierten Abfrage ausgeblendet:', + 'pt-br': 'Os seguintes campos estão ocultos na consulta que você importou:', }, importNoReadPermission: { - "en-us": - "The query you imported contains tables you do not have read access to:", - "es-es": - "La consulta que importó contiene tablas a las que no tiene acceso de lectura:", - "fr-fr": - "La requête que vous avez importée contient des tables auxquelles vous n’avez pas accès en lecture :", - "ru-ru": - "Импортированный вами запрос содержит таблицы, к которым у вас нет доступа на чтение:", - "uk-ua": - "Запит, який ви імпортували, містить таблиці, до яких ви не маєте доступу на читання:", - "de-ch": - "Die von Ihnen importierte Abfrage enthält Tabellen, auf die Sie keinen Lesezugriff haben:", - "pt-br": - "A consulta que você importou contém tabelas às quais você não tem acesso de leitura:", + 'en-us': + 'The query you imported contains tables you do not have read access to:', + 'es-es': + 'La consulta que importó contiene tablas a las que no tiene acceso de lectura:', + 'fr-fr': + 'La requête que vous avez importée contient des tables auxquelles vous n’avez pas accès en lecture :', + 'ru-ru': + 'Импортированный вами запрос содержит таблицы, к которым у вас нет доступа на чтение:', + 'uk-ua': + 'Запит, який ви імпортували, містить таблиці, до яких ви не маєте доступу на читання:', + 'de-ch': + 'Die von Ihnen importierte Abfrage enthält Tabellen, auf die Sie keinen Lesezugriff haben:', + 'pt-br': + 'A consulta que você importou contém tabelas às quais você não tem acesso de leitura:', }, noReadPermission: { - "en-us": "No read permission", - "es-es": "Sin permiso de lectura", - "fr-fr": "Aucune autorisation de lecture", - "ru-ru": "Нет разрешения на чтение", - "uk-ua": "Немає дозволу на читання", - "de-ch": "Keine Leseberechtigung", - "pt-br": "Sem permissão de leitura", + 'en-us': 'No read permission', + 'es-es': 'Sin permiso de lectura', + 'fr-fr': 'Aucune autorisation de lecture', + 'ru-ru': 'Нет разрешения на чтение', + 'uk-ua': 'Немає дозволу на читання', + 'de-ch': 'Keine Leseberechtigung', + 'pt-br': 'Sem permissão de leitura', }, switchToRelative: { - "en-us": "Switch to relative", - "de-ch": "Wechseln zu relativ", - "es-es": "Cambiar a relativo", - "fr-fr": "Passer au relatif", - "ru-ru": "Переключиться на относительный", - "uk-ua": "Перейти до відносного", - "pt-br": "Mudar para relativo", + 'en-us': 'Switch to relative', + 'de-ch': 'Wechseln zu relativ', + 'es-es': 'Cambiar a relativo', + 'fr-fr': 'Passer au relatif', + 'ru-ru': 'Переключиться на относительный', + 'uk-ua': 'Перейти до відносного', + 'pt-br': 'Mudar para relativo', }, switchToAbsolute: { - "en-us": "Switch to absolute", - "de-ch": "Wechseln Sie zu absolut", - "es-es": "Cambiar a absoluto", - "fr-fr": "Passer à l'absolu", - "ru-ru": "Переключиться на абсолютный", - "uk-ua": "Перейти до відносного", - "pt-br": "Mudar para absoluto", + 'en-us': 'Switch to absolute', + 'de-ch': 'Wechseln Sie zu absolut', + 'es-es': 'Cambiar a absoluto', + 'fr-fr': "Passer à l'absolu", + 'ru-ru': 'Переключиться на абсолютный', + 'uk-ua': 'Перейти до відносного', + 'pt-br': 'Mudar para absoluto', }, scrollToEditor: { - "en-us": "Scroll to editor", - "de-ch": "Zum Editor scrollen", - "es-es": "Desplazarse al editor", - "uk-ua": "Перейдіть до редактора", - "fr-fr": "Faites défiler jusqu'à l'éditeur", - "ru-ru": "Прокрутите до редактора", - "pt-br": "Vá até o editor", + 'en-us': 'Scroll to editor', + 'de-ch': 'Zum Editor scrollen', + 'es-es': 'Desplazarse al editor', + 'uk-ua': 'Перейдіть до редактора', + 'fr-fr': "Faites défiler jusqu'à l'éditeur", + 'ru-ru': 'Прокрутите до редактора', + 'pt-br': 'Vá até o editor', }, viewRecords: { - "en-us": "View records", - "de-ch": "Datensätze anzeigen", - "es-es": "Ver registros", - "fr-fr": "Afficher les enregistrements", - "ru-ru": "Просмотреть записи", - "uk-ua": "Переглянути записи", - "pt-br": "Ver registros", + 'en-us': 'View records', + 'de-ch': 'Datensätze anzeigen', + 'es-es': 'Ver registros', + 'fr-fr': 'Afficher les enregistrements', + 'ru-ru': 'Просмотреть записи', + 'uk-ua': 'Переглянути записи', + 'pt-br': 'Ver registros', }, chooseFormatter: { - "en-us": "Choose formatter", - "de-ch": "Formatierer auswählen", - "es-es": "Elija el formateador", - "fr-fr": "Choisir le formateur", - "ru-ru": "Выбрать форматировщик", - "uk-ua": "Виберіть форматер", - "pt-br": "Escolha o formatador", + 'en-us': 'Choose formatter', + 'de-ch': 'Formatierer auswählen', + 'es-es': 'Elija el formateador', + 'fr-fr': 'Choisir le formateur', + 'ru-ru': 'Выбрать форматировщик', + 'uk-ua': 'Виберіть форматер', + 'pt-br': 'Escolha o formatador', }, range: { - "en-us": "Range", - "de-ch": "Reichweite", - "es-es": "Rango", - "fr-fr": "Gamme", - "pt-br": "Faixa", - "ru-ru": "Диапазон", - "uk-ua": "Діапазон", + 'en-us': 'Range', + 'de-ch': 'Reichweite', + 'es-es': 'Rango', + 'fr-fr': 'Gamme', + 'pt-br': 'Faixa', + 'ru-ru': 'Диапазон', + 'uk-ua': 'Діапазон', }, strict: { - "en-us": "Strict", - "de-ch": "Strikt", - "es-es": "Estricto", - "fr-fr": "Strict", - "pt-br": "Estrito", - "ru-ru": "Строгий", - "uk-ua": "Суворий", + 'en-us': 'Strict', + 'de-ch': 'Strikt', + 'es-es': 'Estricto', + 'fr-fr': 'Strict', + 'pt-br': 'Estrito', + 'ru-ru': 'Строгий', + 'uk-ua': 'Суворий', }, nonStrict: { - "en-us": "Non strict", - "de-ch": "Nicht streng", - "es-es": "No estricto", - "fr-fr": "Non strict", - "pt-br": "Não rigoroso", - "ru-ru": "Нестрогий", - "uk-ua": "Не суворий", + 'en-us': 'Non strict', + 'de-ch': 'Nicht streng', + 'es-es': 'No estricto', + 'fr-fr': 'Non strict', + 'pt-br': 'Não rigoroso', + 'ru-ru': 'Нестрогий', + 'uk-ua': 'Не суворий', }, catalogNumberInheritance: { - "en-us": "Catalog Number Inheritance", - "de-ch": "Katalognummernvererbung", - "es-es": "Herencia del número de catálogo", - "fr-fr": "Héritage du numéro de catalogue", - "pt-br": "Herança de números de catálogo", - "ru-ru": "Наследование каталожного номера", - "uk-ua": "Успадкування каталожних номерів", + 'en-us': 'Catalog Number Inheritance', + 'de-ch': 'Katalognummernvererbung', + 'es-es': 'Herencia del número de catálogo', + 'fr-fr': 'Héritage du numéro de catalogue', + 'pt-br': 'Herança de números de catálogo', + 'ru-ru': 'Наследование каталожного номера', + 'uk-ua': 'Успадкування каталожних номерів', }, uniqueCatalogNumberAcrossComponentAndCo: { - "en-us": "Catalog Number Uniqueness Across Component And CO tables", - "de-ch": "Eindeutigkeit der Katalognummer in Komponenten- und CO-Tabellen", - "es-es": - "Unicidad del número de catálogo en las tablas de componentes y CO", - "fr-fr": - "Unicité des numéros de catalogue entre les tableaux de composants et de CO", - "pt-br": - "Exclusividade do número de catálogo nas tabelas de componentes e CO", - "ru-ru": "Уникальность каталожного номера в таблицах компонентов и CO", - "uk-ua": "Унікальність каталожних номерів у таблицях компонентів та CO", + 'en-us': 'Catalog Number Uniqueness Across Component And CO tables', + 'de-ch': 'Eindeutigkeit der Katalognummer in Komponenten- und CO-Tabellen', + 'es-es': + 'Unicidad del número de catálogo en las tablas de componentes y CO', + 'fr-fr': + 'Unicité des numéros de catalogue entre les tableaux de composants et de CO', + 'pt-br': + 'Exclusividade do número de catálogo nas tabelas de componentes e CO', + 'ru-ru': 'Уникальность каталожного номера в таблицах компонентов и CO', + 'uk-ua': 'Унікальність каталожних номерів у таблицях компонентів та CO', }, formatInputAs: { comment: ` @@ -975,52 +975,52 @@ export const queryText = createDictionary({ Example: Format As: Ichthyology Example: Format As: Rock, Mineral `, - "en-us": "Format As: {commaSeparatedFormats:string}", - "de-ch": "Formatieren als: {commaSeparatedFormats:string}", - "es-es": "Formato como: {commaSeparatedFormats:string}", - "fr-fr": "Formater comme : {commaSeparatedFormats:string}", - "pt-br": "Formato como: {commaSeparatedFormats:string}", - "ru-ru": "Форматировать как: {commaSeparatedFormats:string}", - "uk-ua": "Форматувати як: {commaSeparatedFormats:string}", + 'en-us': 'Format As: {commaSeparatedFormats:string}', + 'de-ch': 'Formatieren als: {commaSeparatedFormats:string}', + 'es-es': 'Formato como: {commaSeparatedFormats:string}', + 'fr-fr': 'Formater comme : {commaSeparatedFormats:string}', + 'pt-br': 'Formato como: {commaSeparatedFormats:string}', + 'ru-ru': 'Форматировать как: {commaSeparatedFormats:string}', + 'uk-ua': 'Форматувати як: {commaSeparatedFormats:string}', }, unsavedChangesInQuery: { - "en-us": "Query has unsaved changes", - "de-ch": "Die Abfrage enthält nicht gespeicherte Änderungen", - "es-es": "La consulta tiene cambios sin guardar", - "fr-fr": "La requête comporte des modifications non enregistrées", - "pt-br": "A consulta possui alterações não salvas", - "ru-ru": "Запрос имеет несохраненные изменения", - "uk-ua": "Запит містить незбережені зміни", + 'en-us': 'Query has unsaved changes', + 'de-ch': 'Die Abfrage enthält nicht gespeicherte Änderungen', + 'es-es': 'La consulta tiene cambios sin guardar', + 'fr-fr': 'La requête comporte des modifications non enregistrées', + 'pt-br': 'A consulta possui alterações não salvas', + 'ru-ru': 'Запрос имеет несохраненные изменения', + 'uk-ua': 'Запит містить незбережені зміни', }, unsavedChangesInQueryDescription: { - "en-us": "Please save the query before running Batch Edit", - "de-ch": - "Bitte speichern Sie die Abfrage, bevor Sie die Stapelbearbeitung ausführen", - "es-es": "Guarde la consulta antes de ejecutar la edición por lotes", - "fr-fr": + 'en-us': 'Please save the query before running Batch Edit', + 'de-ch': + 'Bitte speichern Sie die Abfrage, bevor Sie die Stapelbearbeitung ausführen', + 'es-es': 'Guarde la consulta antes de ejecutar la edición por lotes', + 'fr-fr': "Veuillez enregistrer la requête avant d'exécuter l'édition par lots", - "pt-br": "Salve a consulta antes de executar a edição em lote", - "ru-ru": - "Пожалуйста, сохраните запрос перед запуском пакетного редактирования.", - "uk-ua": "Будь ласка, збережіть запит перед запуском пакетного редагування", + 'pt-br': 'Salve a consulta antes de executar a edição em lote', + 'ru-ru': + 'Пожалуйста, сохраните запрос перед запуском пакетного редактирования.', + 'uk-ua': 'Будь ласка, збережіть запит перед запуском пакетного редагування', }, noPreparationsToReturn: { - "en-us": "There are no unresolved items to return", - "ru-ru": "Нет нерешенных вопросов для возврата", - "es-es": "No hay items sin resolver para devolver", - "fr-fr": "Il n'y a aucun article non résolu à retourner", - "uk-ua": "Немає невирішених елементів для повернення", - "de-ch": - "Es gibt keine ungelösten Elemente, die zurückgegeben werden müssen", - "pt-br": "Não há itens não resolvidos para retornar", + 'en-us': 'There are no unresolved items to return', + 'ru-ru': 'Нет нерешенных вопросов для возврата', + 'es-es': 'No hay items sin resolver para devolver', + 'fr-fr': "Il n'y a aucun article non résolu à retourner", + 'uk-ua': 'Немає невирішених елементів для повернення', + 'de-ch': + 'Es gibt keine ungelösten Elemente, die zurückgegeben werden müssen', + 'pt-br': 'Não há itens não resolvidos para retornar', }, itemsReturned: { - "en-us": "Items have been returned", - "ru-ru": "Товары были возвращены", - "es-es": "Los items han sido devueltos", - "fr-fr": "Les articles ont été retournés", - "uk-ua": "Товари повернуто", - "de-ch": "Artikel wurden zurückgegeben", - "pt-br": "Os itens foram devolvidos", + 'en-us': 'Items have been returned', + 'ru-ru': 'Товары были возвращены', + 'es-es': 'Los items han sido devueltos', + 'fr-fr': 'Les articles ont été retournés', + 'uk-ua': 'Товари повернуто', + 'de-ch': 'Artikel wurden zurückgegeben', + 'pt-br': 'Os itens foram devolvidos', }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/tree.ts b/specifyweb/frontend/js_src/lib/localization/tree.ts index 855a009bf51..4a3f2737142 100644 --- a/specifyweb/frontend/js_src/lib/localization/tree.ts +++ b/specifyweb/frontend/js_src/lib/localization/tree.ts @@ -4,704 +4,704 @@ * @module */ -import { createDictionary } from "./utils"; +import { createDictionary } from './utils'; // Refer to "Guidelines for Programmers" in ./README.md before editing this file export const treeText = createDictionary({ trees: { - "en-us": "Trees", - "ru-ru": "Деревья", - "es-es": "Árboles", - "fr-fr": "Arbres", - "uk-ua": "дерева", - "de-ch": "Hierarchien", - "pt-br": "Árvores", + 'en-us': 'Trees', + 'ru-ru': 'Деревья', + 'es-es': 'Árboles', + 'fr-fr': 'Arbres', + 'uk-ua': 'дерева', + 'de-ch': 'Hierarchien', + 'pt-br': 'Árvores', }, badStructure: { - "en-us": "Bad tree structure.", - "ru-ru": "Плохая структура дерева.", - "es-es": "Estructura de árbol incorrecta.", - "fr-fr": "Mauvaise arborescence.", - "uk-ua": "Погана структура дерева.", - "de-ch": "Fehlerhafte Baumstruktur.", - "pt-br": "Estrutura de árvore ruim.", + 'en-us': 'Bad tree structure.', + 'ru-ru': 'Плохая структура дерева.', + 'es-es': 'Estructura de árbol incorrecta.', + 'fr-fr': 'Mauvaise arborescence.', + 'uk-ua': 'Погана структура дерева.', + 'de-ch': 'Fehlerhafte Baumstruktur.', + 'pt-br': 'Estrutura de árvore ruim.', }, move: { - "en-us": "Move", - "ru-ru": "Двигаться", - "es-es": "Mover", - "fr-fr": "Déplacer", - "uk-ua": "рухатися", - "de-ch": "Verschieben", - "pt-br": "Mover", + 'en-us': 'Move', + 'ru-ru': 'Двигаться', + 'es-es': 'Mover', + 'fr-fr': 'Déplacer', + 'uk-ua': 'рухатися', + 'de-ch': 'Verschieben', + 'pt-br': 'Mover', }, merge: { - "en-us": "Merge", - "ru-ru": "Слияние", - "es-es": "Unir", - "fr-fr": "Fusionner", - "uk-ua": "Об’єднати", - "de-ch": "Zusammenführen", - "pt-br": "Mesclar", + 'en-us': 'Merge', + 'ru-ru': 'Слияние', + 'es-es': 'Unir', + 'fr-fr': 'Fusionner', + 'uk-ua': 'Об’єднати', + 'de-ch': 'Zusammenführen', + 'pt-br': 'Mesclar', }, undoSynonymy: { - "en-us": "Undo Synonymy", - "ru-ru": "Отменить синонимию", - "es-es": "Deshacer sinonimia", - "fr-fr": "Annuler la synonymie", - "uk-ua": "Скасувати синонімію", - "de-ch": "Synonymie rückgängig machen", - "pt-br": "Desfazer Sinonímia", + 'en-us': 'Undo Synonymy', + 'ru-ru': 'Отменить синонимию', + 'es-es': 'Deshacer sinonimia', + 'fr-fr': 'Annuler la synonymie', + 'uk-ua': 'Скасувати синонімію', + 'de-ch': 'Synonymie rückgängig machen', + 'pt-br': 'Desfazer Sinonímia', }, synonymize: { - "en-us": "Synonymize", - "ru-ru": "Подбирать синонимы", - "es-es": "Sinonimizar", - "fr-fr": "Synonymiser", - "uk-ua": "Синонімізувати", - "de-ch": "Synonymisieren", - "pt-br": "Sinonímia", + 'en-us': 'Synonymize', + 'ru-ru': 'Подбирать синонимы', + 'es-es': 'Sinonimizar', + 'fr-fr': 'Synonymiser', + 'uk-ua': 'Синонімізувати', + 'de-ch': 'Synonymisieren', + 'pt-br': 'Sinonímia', }, actionFailed: { - "en-us": "Operation failed", - "ru-ru": "Операция провалилась", - "es-es": "Operación fallida", - "fr-fr": "L'opération a échoué", - "uk-ua": "Операція не вдалася", - "de-ch": "Vorgang fehlgeschlagen", - "pt-br": "A operação falhou", + 'en-us': 'Operation failed', + 'ru-ru': 'Операция провалилась', + 'es-es': 'Operación fallida', + 'fr-fr': "L'opération a échoué", + 'uk-ua': 'Операція не вдалася', + 'de-ch': 'Vorgang fehlgeschlagen', + 'pt-br': 'A operação falhou', }, actionFailedDescription: { - "en-us": - "The operation could not be completed due to the following errors:", - "ru-ru": "Операция не может быть завершена из-за следующих ошибок:", - "es-es": - "La operación no se pudo completar debido a los siguientes errores:", - "fr-fr": + 'en-us': + 'The operation could not be completed due to the following errors:', + 'ru-ru': 'Операция не может быть завершена из-за следующих ошибок:', + 'es-es': + 'La operación no se pudo completar debido a los siguientes errores:', + 'fr-fr': "L'opération n'a pas pu être terminée en raison des erreurs suivantes :", - "de-ch": - "Der Vorgang konnte aufgrund der folgenden Fehler nicht ausgeführt werden:", - "uk-ua": "Операцію не вдалося завершити через такі помилки:", - "pt-br": "A operação não pôde ser concluída devido aos seguintes erros:", + 'de-ch': + 'Der Vorgang konnte aufgrund der folgenden Fehler nicht ausgeführt werden:', + 'uk-ua': 'Операцію не вдалося завершити через такі помилки:', + 'pt-br': 'A operação não pôde ser concluída devido aos seguintes erros:', }, moveNode: { - "en-us": "Move node", - "ru-ru": "Переместить узел", - "es-es": "Mover nodo", - "fr-fr": "Déplacer le nœud", - "uk-ua": "Перемістити вузол", - "de-ch": "Knoten verschieben", - "pt-br": "Mover nó", + 'en-us': 'Move node', + 'ru-ru': 'Переместить узел', + 'es-es': 'Mover nodo', + 'fr-fr': 'Déplacer le nœud', + 'uk-ua': 'Перемістити вузол', + 'de-ch': 'Knoten verschieben', + 'pt-br': 'Mover nó', }, addChild: { - "en-us": "Add Child", - "ru-ru": "Добавить ребенка", - "es-es": "Agregar hijo", - "fr-fr": "Ajouter un enfant", - "uk-ua": "Додати дитину", - "de-ch": "Kind hinzuzufügen", - "pt-br": "Adicionar criança", + 'en-us': 'Add Child', + 'ru-ru': 'Добавить ребенка', + 'es-es': 'Agregar hijo', + 'fr-fr': 'Ajouter un enfant', + 'uk-ua': 'Додати дитину', + 'de-ch': 'Kind hinzuzufügen', + 'pt-br': 'Adicionar criança', }, moveNodeHere: { - "en-us": 'Move "{nodeName:string}" here', - "ru-ru": "Переместите «{nodeName:string}» сюда", - "es-es": 'Mover "{nodeName:string}" aquí', - "fr-fr": "Déplacer « {nodeName:string} » ici", - "uk-ua": 'Перемістіть сюди "{nodeName:string}"', - "de-ch": 'Verschiebe "{nodeName:string}" hierhin', - "pt-br": 'Mova "{nodeName:string}" aqui', + 'en-us': 'Move "{nodeName:string}" here', + 'ru-ru': 'Переместите «{nodeName:string}» сюда', + 'es-es': 'Mover "{nodeName:string}" aquí', + 'fr-fr': 'Déplacer « {nodeName:string} » ici', + 'uk-ua': 'Перемістіть сюди "{nodeName:string}"', + 'de-ch': 'Verschiebe "{nodeName:string}" hierhin', + 'pt-br': 'Mova "{nodeName:string}" aqui', }, moveNodePreparationsHere: { - "en-us": 'Move all "{nodeName:string}" preparations here', - "de-ch": 'Verschieben Sie alle "{nodeName:string}"-Vorbereitungen hierher', - "es-es": 'Mover todas las preparaciones "{nodeName:string}" aquí', - "fr-fr": "Déplacer toutes les préparations « {nodeName:string} » ici", - "ru-ru": "Переместить все препараты «{nodeName:string}» сюда", - "uk-ua": 'Перемістіть сюди всі препарати "{nodeName:string}"', - "pt-br": 'Mova todos os preparativos "{nodeName:string}" aqui', + 'en-us': 'Move all "{nodeName:string}" preparations here', + 'de-ch': 'Verschieben Sie alle "{nodeName:string}"-Vorbereitungen hierher', + 'es-es': 'Mover todas las preparaciones "{nodeName:string}" aquí', + 'fr-fr': 'Déplacer toutes les préparations « {nodeName:string} » ici', + 'ru-ru': 'Переместить все препараты «{nodeName:string}» сюда', + 'uk-ua': 'Перемістіть сюди всі препарати "{nodeName:string}"', + 'pt-br': 'Mova todos os preparativos "{nodeName:string}" aqui', }, nodeMoveMessage: { - "en-us": + 'en-us': 'The {treeName:string} node "{nodeName:string}" will be placed, along with all of its descendants, under the new parent "{parentName:string}".', - "ru-ru": - "Узел {treeName:string} «{nodeName:string}» будет помещен вместе со всеми его потомками под новый родительский узел «{parentName:string}».", - "es-es": + 'ru-ru': + 'Узел {treeName:string} «{nodeName:string}» будет помещен вместе со всеми его потомками под новый родительский узел «{parentName:string}».', + 'es-es': 'El nodo {treeName:string} "{nodeName:string}" se colocará, junto con todos sus descendientes, bajo el nuevo padre "{parentName:string}".', - "fr-fr": - "Le nœud {treeName:string} « {nodeName:string} » sera placé, ainsi que tous ses descendants, sous le nouveau parent « {parentName:string} ».", - "uk-ua": + 'fr-fr': + 'Le nœud {treeName:string} « {nodeName:string} » sera placé, ainsi que tous ses descendants, sous le nouveau parent « {parentName:string} ».', + 'uk-ua': 'Вузол {treeName:string} "{nodeName:string}" буде розміщено разом із усіма його нащадками під новим батьківським вузлом "{parentName:string}".', - "de-ch": + 'de-ch': 'Der {treeName:string} Knoten "{nodeName:string}" wird zusammen mit allen seinen Unterknoten unter den neuen übergeordneten Knoten "{parentName:string}" platziert.', - "pt-br": + 'pt-br': 'O nó {treeName:string} "{nodeName:string}" será colocado, junto com todos os seus descendentes, sob o novo pai "{parentName:string}".', }, nodeBulkMoveMessage: { - "en-us": + 'en-us': 'The {treeName:string} node "{nodeName:string}" preparations will be placed under the new location "{parentName:string}".', - "de-ch": - "Die Vorbereitungen für den {treeName:string}-Knoten „{nodeName:string}“ werden unter dem neuen Standort „{parentName:string}“ platziert.", - "es-es": + 'de-ch': + 'Die Vorbereitungen für den {treeName:string}-Knoten „{nodeName:string}“ werden unter dem neuen Standort „{parentName:string}“ platziert.', + 'es-es': 'Las preparaciones del nodo {treeName:string} "{nodeName:string}" se colocarán en la nueva ubicación "{parentName:string}".', - "fr-fr": - "Les préparations du nœud {treeName:string} « {nodeName:string} » seront placées sous le nouvel emplacement « {parentName:string} ».", - "ru-ru": - "Заготовки узла {treeName:string} «{nodeName:string}» будут размещены в новом месте «{parentName:string}».", - "uk-ua": + 'fr-fr': + 'Les préparations du nœud {treeName:string} « {nodeName:string} » seront placées sous le nouvel emplacement « {parentName:string} ».', + 'ru-ru': + 'Заготовки узла {treeName:string} «{nodeName:string}» будут размещены в новом месте «{parentName:string}».', + 'uk-ua': 'Підготовка вузла {treeName:string} "{nodeName:string}" буде розміщена в новому місці розташування "{parentName:string}".', - "pt-br": + 'pt-br': 'Os preparativos do nó {treeName:string} "{nodeName:string}" serão colocados no novo local "{parentName:string}".', }, cantMoveHere: { - "en-us": "Can't move this tree node here", - "ru-ru": "Невозможно переместить этот узел дерева сюда.", - "es-es": "No se puede mover este nodo del árbol aquí", - "fr-fr": "Impossible de déplacer ce nœud d'arbre ici", - "uk-ua": "Неможливо перемістити цей вузол дерева сюди", - "de-ch": "Dieser Knoten kann nicht hierhin verschoben werden", - "pt-br": "Não é possível mover este nó da árvore aqui", + 'en-us': "Can't move this tree node here", + 'ru-ru': 'Невозможно переместить этот узел дерева сюда.', + 'es-es': 'No se puede mover este nodo del árbol aquí', + 'fr-fr': "Impossible de déplacer ce nœud d'arbre ici", + 'uk-ua': 'Неможливо перемістити цей вузол дерева сюди', + 'de-ch': 'Dieser Knoten kann nicht hierhin verschoben werden', + 'pt-br': 'Não é possível mover este nó da árvore aqui', }, cantMergeHere: { - "en-us": "Can't merge this tree node here", - "ru-ru": "Невозможно объединить этот узел дерева здесь.", - "es-es": "No se puede fusionar este nodo de árbol aquí", - "fr-fr": "Impossible de fusionner ce nœud d'arbre ici", - "uk-ua": "Неможливо об’єднати цей вузол дерева тут", - "de-ch": "Dieser Knoten kann hier nicht zusammengelegt werden", - "pt-br": "Não é possível mesclar este nó da árvore aqui", + 'en-us': "Can't merge this tree node here", + 'ru-ru': 'Невозможно объединить этот узел дерева здесь.', + 'es-es': 'No se puede fusionar este nodo de árbol aquí', + 'fr-fr': "Impossible de fusionner ce nœud d'arbre ici", + 'uk-ua': 'Неможливо об’єднати цей вузол дерева тут', + 'de-ch': 'Dieser Knoten kann hier nicht zusammengelegt werden', + 'pt-br': 'Não é possível mesclar este nó da árvore aqui', }, cantMoveToSynonym: { - "en-us": "Can't move to a synonym", - "ru-ru": "Невозможно перейти к синониму", - "es-es": "No se puede mover a un sinónimo", - "fr-fr": "Impossible de passer à un synonyme", - "uk-ua": "Неможливо перейти до синоніма", - "de-ch": "Kann nicht zu einem Synonym verschieben", - "pt-br": "Não é possível mover para um sinônimo", + 'en-us': "Can't move to a synonym", + 'ru-ru': 'Невозможно перейти к синониму', + 'es-es': 'No se puede mover a un sinónimo', + 'fr-fr': 'Impossible de passer à un synonyme', + 'uk-ua': 'Неможливо перейти до синоніма', + 'de-ch': 'Kann nicht zu einem Synonym verschieben', + 'pt-br': 'Não é possível mover para um sinônimo', }, cantMergeIntoSynonym: { - "en-us": "Can't merge into synonyms", - "ru-ru": "Невозможно объединить в синонимы", - "es-es": "No se pueden fusionar en sinónimos", - "fr-fr": "Impossible de fusionner en synonymes", - "uk-ua": "Не можна об’єднувати в синоніми", - "de-ch": "Kann nicht zu Synonymen zusammenführen", - "pt-br": "Não é possível mesclar em sinônimos", + 'en-us': "Can't merge into synonyms", + 'ru-ru': 'Невозможно объединить в синонимы', + 'es-es': 'No se pueden fusionar en sinónimos', + 'fr-fr': 'Impossible de fusionner en synonymes', + 'uk-ua': 'Не можна об’єднувати в синоніми', + 'de-ch': 'Kann nicht zu Synonymen zusammenführen', + 'pt-br': 'Não é possível mesclar em sinônimos', }, cantSynonymizeSynonym: { - "en-us": "Can't synonymize with a synonym", - "ru-ru": "Невозможно синонимизировать с помощью синонима", - "es-es": "No se puede sinonimizar con un sinónimo", - "fr-fr": "Impossible de synonymiser avec un synonyme", - "uk-ua": "Не можна синонімізувати синонім", - "de-ch": "Kann nicht mit einem Synonym synonymisiert werden", - "pt-br": "Não é possível sinonimizar com um sinônimo", + 'en-us': "Can't synonymize with a synonym", + 'ru-ru': 'Невозможно синонимизировать с помощью синонима', + 'es-es': 'No se puede sinonimizar con un sinónimo', + 'fr-fr': 'Impossible de synonymiser avec un synonyme', + 'uk-ua': 'Не можна синонімізувати синонім', + 'de-ch': 'Kann nicht mit einem Synonym synonymisiert werden', + 'pt-br': 'Não é possível sinonimizar com um sinônimo', }, nodeMoveHintMessage: { - "en-us": 'Select a new parent for "{nodeName:string}"', - "ru-ru": "Выберите нового родителя для «{nodeName:string}»", - "es-es": 'Seleccione un nuevo padre para "{nodeName:string}"', - "fr-fr": "Sélectionnez un nouveau parent pour « {nodeName:string} »", - "uk-ua": 'Виберіть новий батьківський елемент для "{nodeName:string}"', - "de-ch": 'Wählen Sie ein neues Elternelement für "{nodeName:string}"', - "pt-br": 'Selecione um novo pai para "{nodeName:string}"', + 'en-us': 'Select a new parent for "{nodeName:string}"', + 'ru-ru': 'Выберите нового родителя для «{nodeName:string}»', + 'es-es': 'Seleccione un nuevo padre para "{nodeName:string}"', + 'fr-fr': 'Sélectionnez un nouveau parent pour « {nodeName:string} »', + 'uk-ua': 'Виберіть новий батьківський елемент для "{nodeName:string}"', + 'de-ch': 'Wählen Sie ein neues Elternelement für "{nodeName:string}"', + 'pt-br': 'Selecione um novo pai para "{nodeName:string}"', }, mergeNode: { - "en-us": "Merge node", - "ru-ru": "Узел слияния", - "es-es": "Fusionar el nodo", - "fr-fr": "Fusionner le nœud", - "uk-ua": "Вузол злиття", - "de-ch": "Knoten zusammenführen", - "pt-br": "Nó de mesclagem", + 'en-us': 'Merge node', + 'ru-ru': 'Узел слияния', + 'es-es': 'Fusionar el nodo', + 'fr-fr': 'Fusionner le nœud', + 'uk-ua': 'Вузол злиття', + 'de-ch': 'Knoten zusammenführen', + 'pt-br': 'Nó de mesclagem', }, mergeNodeHere: { - "en-us": 'Merge "{nodeName:string}" here', - "ru-ru": "Объединить «{nodeName:string}» здесь", - "es-es": 'Fusionar "{nodeName:string}" aquí', - "fr-fr": "Fusionner « {nodeName:string} » ici", - "uk-ua": 'Об\'єднайте "{nodeName:string}" тут', - "de-ch": 'Führe "{nodeName:string}" hier zusammen', - "pt-br": 'Mesclar "{nodeName:string}" aqui', + 'en-us': 'Merge "{nodeName:string}" here', + 'ru-ru': 'Объединить «{nodeName:string}» здесь', + 'es-es': 'Fusionar "{nodeName:string}" aquí', + 'fr-fr': 'Fusionner « {nodeName:string} » ici', + 'uk-ua': 'Об\'єднайте "{nodeName:string}" тут', + 'de-ch': 'Führe "{nodeName:string}" hier zusammen', + 'pt-br': 'Mesclar "{nodeName:string}" aqui', }, mergeNodeHintMessage: { - "en-us": 'Select a new target for "{nodeName:string}" to be merged into', - "ru-ru": "Выберите новую цель для объединения «{nodeName:string}»", - "es-es": 'Seleccione un nuevo objetivo para fusionar "{nodeName:string}"', - "fr-fr": - "Sélectionnez une nouvelle cible dans laquelle « {nodeName:string} » doit être fusionné", - "uk-ua": - "Виберіть нову ціль для «{nodeName:string}», у яку потрібно об’єднати", - "de-ch": 'Wähle ein neues Ziel um "{nodeName:string}" zusammenzuführen', - "pt-br": 'Selecione um novo alvo para "{nodeName:string}" a ser mesclado', + 'en-us': 'Select a new target for "{nodeName:string}" to be merged into', + 'ru-ru': 'Выберите новую цель для объединения «{nodeName:string}»', + 'es-es': 'Seleccione un nuevo objetivo para fusionar "{nodeName:string}"', + 'fr-fr': + 'Sélectionnez une nouvelle cible dans laquelle « {nodeName:string} » doit être fusionné', + 'uk-ua': + 'Виберіть нову ціль для «{nodeName:string}», у яку потрібно об’єднати', + 'de-ch': 'Wähle ein neues Ziel um "{nodeName:string}" zusammenzuführen', + 'pt-br': 'Selecione um novo alvo para "{nodeName:string}" a ser mesclado', }, bulkMoveNodeHintMessage: { - "en-us": + 'en-us': 'Select a new target for "{nodeName:string}" preparations to be moved into', - "de-ch": + 'de-ch': 'Wählen Sie ein neues Ziel für die "{nodeName:string}"-Vorbereitungen, in die Sie verschieben möchten', - "es-es": + 'es-es': 'Seleccione un nuevo objetivo para los preparaciones "{nodeName:string}" que se trasladarán a', - "fr-fr": - "Sélectionnez une nouvelle cible pour les préparations « {nodeName:string} » à déplacer", - "ru-ru": - "Выберите новую цель для перемещения препаратов «{nodeName:string}»", - "uk-ua": + 'fr-fr': + 'Sélectionnez une nouvelle cible pour les préparations « {nodeName:string} » à déplacer', + 'ru-ru': + 'Выберите новую цель для перемещения препаратов «{nodeName:string}»', + 'uk-ua': 'Виберіть нову ціль для препаратів "{nodeName:string}", до якої потрібно переміститися', - "pt-br": + 'pt-br': 'Selecione um novo alvo para os preparativos "{nodeName:string}" a serem movidos para', }, mergeNodeMessage: { - "en-us": + 'en-us': 'All references to {treeName:string} node "{nodeName:string}" will be replaced with "{parentName:string}", and all descendants of "{nodeName:string}" will be moved to "{parentName:string}" with any descendants matching in name and rank being themselves merged recursively.', - "ru-ru": - "Все ссылки на узел {treeName:string} «{nodeName:string}» будут заменены на «{parentName:string}», а все потомки «{nodeName:string}» будут перемещены в «{parentName:string}», при этом все потомки, совпадающие по имени и рангу, будут рекурсивно объединены.", - "es-es": + 'ru-ru': + 'Все ссылки на узел {treeName:string} «{nodeName:string}» будут заменены на «{parentName:string}», а все потомки «{nodeName:string}» будут перемещены в «{parentName:string}», при этом все потомки, совпадающие по имени и рангу, будут рекурсивно объединены.', + 'es-es': 'Todas las referencias al nodo {treeName:string} "{nodeName:string}" serán reemplazadas por "{parentName:string}", todos los descendientes de "{nodeName:string}" se moverán a "{parentName:string}" y todos los descendientes que coincidan en nombre y rango se fusionarán de forma recursiva.', - "fr-fr": - "Toutes les références au nœud {treeName:string} « {nodeName:string} » seront remplacées par « {parentName:string} », et tous les descendants de « {nodeName:string} » seront déplacés vers « {parentName:string} », tous les descendants correspondant au nom et au rang étant eux-mêmes fusionnés de manière récursive.", - "uk-ua": + 'fr-fr': + 'Toutes les références au nœud {treeName:string} « {nodeName:string} » seront remplacées par « {parentName:string} », et tous les descendants de « {nodeName:string} » seront déplacés vers « {parentName:string} », tous les descendants correspondant au nom et au rang étant eux-mêmes fusionnés de manière récursive.', + 'uk-ua': "Усі посилання на вузол {treeName:string} «{nodeName:string}» буде замінено на «{parentName:string}», а всі нащадки «{nodeName:string}» буде переміщено до «{parentName:string}», а будь-які нащадки, що відповідають імені та рангу, будуть самі собою об'єднані рекурсивно.", - "de-ch": + 'de-ch': 'Alle Referenzen zu {treeName:string} "{nodeName:string}" werden mit "{parentName:string}" ersetzt. Alle Nachkommen von "{nodeName:string}" werden nach "{parentName:string}" verschoben, wobei alle Nachkommen, die in Name und Rang übereinstimmen, selbst rekursiv zusammengeführt werden.', - "pt-br": + 'pt-br': 'Todas as referências ao nó {treeName:string} "{nodeName:string}" serão substituídas por "{parentName:string}", e todos os descendentes de "{nodeName:string}" serão movidos para "{parentName:string}", com quaisquer descendentes correspondentes em nome e classificação sendo eles próprios mesclados recursivamente.', }, synonymizeNode: { - "en-us": "Synonymize node", - "ru-ru": "Узел синонимизации", - "es-es": "Sinonimizar nodo", - "fr-fr": "Synonymiser le nœud", - "uk-ua": "Синонімізувати вузол", - "de-ch": "Knoten synonymisieren", - "pt-br": "Sinonímia do nó", + 'en-us': 'Synonymize node', + 'ru-ru': 'Узел синонимизации', + 'es-es': 'Sinonimizar nodo', + 'fr-fr': 'Synonymiser le nœud', + 'uk-ua': 'Синонімізувати вузол', + 'de-ch': 'Knoten synonymisieren', + 'pt-br': 'Sinonímia do nó', }, makeSynonym: { - "en-us": "Make {nodeName:string} a synonym of {synonymName:string}", - "ru-ru": "Сделайте {nodeName:string} синонимом {synonymName:string}", - "es-es": "Hacer de {nodeName:string} un sinónimo de {synonymName:string}", - "fr-fr": "Faire de {nodeName:string} un synonyme de {synonymName:string}", - "uk-ua": "Зробити {nodeName:string} синонімом {synonymName:string}", - "de-ch": - "Aus {nodeName:string} ein Synonym von {synonymName:string} machen", - "pt-br": "Faça {nodeName:string} um sinônimo de {synonymName:string}", + 'en-us': 'Make {nodeName:string} a synonym of {synonymName:string}', + 'ru-ru': 'Сделайте {nodeName:string} синонимом {synonymName:string}', + 'es-es': 'Hacer de {nodeName:string} un sinónimo de {synonymName:string}', + 'fr-fr': 'Faire de {nodeName:string} un synonyme de {synonymName:string}', + 'uk-ua': 'Зробити {nodeName:string} синонімом {synonymName:string}', + 'de-ch': + 'Aus {nodeName:string} ein Synonym von {synonymName:string} machen', + 'pt-br': 'Faça {nodeName:string} um sinônimo de {synonymName:string}', }, synonymizeNodeHintMessage: { - "en-us": 'Select a target for "{nodeName:string}" to be synonymized to', - "ru-ru": "Выберите цель для синонимизации «{nodeName:string}»", - "es-es": + 'en-us': 'Select a target for "{nodeName:string}" to be synonymized to', + 'ru-ru': 'Выберите цель для синонимизации «{nodeName:string}»', + 'es-es': 'Seleccione un objetivo para que "{nodeName:string}" se convierta en sinónimo', - "fr-fr": - "Sélectionnez une cible pour laquelle « {nodeName:string} » doit être synonymisé", - "uk-ua": 'Виберіть ціль для "{nodeName:string}", який буде синонімічним', - "de-ch": 'Wähle Ziel um "{nodeName:string}" daran zu synonymisieren', - "pt-br": 'Selecione um alvo para "{nodeName:string}" ser sinonimizado', + 'fr-fr': + 'Sélectionnez une cible pour laquelle « {nodeName:string} » doit être synonymisé', + 'uk-ua': 'Виберіть ціль для "{nodeName:string}", який буде синонімічним', + 'de-ch': 'Wähle Ziel um "{nodeName:string}" daran zu synonymisieren', + 'pt-br': 'Selecione um alvo para "{nodeName:string}" ser sinonimizado', }, synonymizeMessage: { - "en-us": + 'en-us': 'The {treeName:string} node "{nodeName:string}" will be made a synonym of "{synonymName:string}".', - "ru-ru": - "Узел {treeName:string} «{nodeName:string}» станет синонимом «{synonymName:string}».", - "es-es": + 'ru-ru': + 'Узел {treeName:string} «{nodeName:string}» станет синонимом «{synonymName:string}».', + 'es-es': 'El nodo {treeName:string} "{nodeName:string}" se convertirá en sinónimo de "{synonymName:string}".', - "fr-fr": - "Le nœud {treeName:string} « {nodeName:string} » deviendra synonyme de « {synonymName:string} ».", - "de-ch": + 'fr-fr': + 'Le nœud {treeName:string} « {nodeName:string} » deviendra synonyme de « {synonymName:string} ».', + 'de-ch': 'Der {treeName:string}-Knoten "{nodeName:string}" wird zu einem Synonym von "{synonymName:string}".', - "uk-ua": + 'uk-ua': 'Вузол {treeName:string} "{nodeName:string}" стане синонімом "{synonymName:string}".', - "pt-br": + 'pt-br': 'O nó {treeName:string} "{nodeName:string}" se tornará sinônimo de "{synonymName:string}".', }, desynonymizeNode: { - "en-us": "Desynonymize node", - "ru-ru": "Узел десиномизации", - "es-es": "Desinonimizar nodo", - "fr-fr": "Désynonymiser le nœud", - "uk-ua": "Десинонімізувати вузол", - "de-ch": "Knoten desynonymisieren", - "pt-br": "Dessinonimizar nó", + 'en-us': 'Desynonymize node', + 'ru-ru': 'Узел десиномизации', + 'es-es': 'Desinonimizar nodo', + 'fr-fr': 'Désynonymiser le nœud', + 'uk-ua': 'Десинонімізувати вузол', + 'de-ch': 'Knoten desynonymisieren', + 'pt-br': 'Dessinonimizar nó', }, desynonymizeNodeMessage: { - "en-us": + 'en-us': '"{nodeName:string}" will no longer be a synonym of "{synonymName:string}".', - "ru-ru": - "«{nodeName:string}» больше не будет синонимом «{synonymName:string}».", - "es-es": + 'ru-ru': + '«{nodeName:string}» больше не будет синонимом «{synonymName:string}».', + 'es-es': '"{nodeName:string}" ya no será sinónimo de "{synonymName:string}".', - "fr-fr": - "« {nodeName:string} » ne sera plus synonyme de « {synonymName:string} ».", - "de-ch": + 'fr-fr': + '« {nodeName:string} » ne sera plus synonyme de « {synonymName:string} ».', + 'de-ch': '"{nodeName:string}" wird nicht mehr ein Synonym von "{synonymName:string}" sein.', - "uk-ua": + 'uk-ua': '"{nodeName:string}" більше не буде синонімом "{synonymName:string}".', - "pt-br": + 'pt-br': '"{nodeName:string}" não será mais sinônimo de "{synonymName:string}".', }, acceptedName: { - "en-us": "Preferred: {name:string}", - "ru-ru": "Предпочтительно: {name:string}", - "es-es": "Preferido: {name:string}", - "fr-fr": "Préféré : {name:string}", - "uk-ua": "Бажано: {name:string}", - "de-ch": "Bevorzugt: {name:string}", - "pt-br": "Preferido: {name:string}", + 'en-us': 'Preferred: {name:string}', + 'ru-ru': 'Предпочтительно: {name:string}', + 'es-es': 'Preferido: {name:string}', + 'fr-fr': 'Préféré : {name:string}', + 'uk-ua': 'Бажано: {name:string}', + 'de-ch': 'Bevorzugt: {name:string}', + 'pt-br': 'Preferido: {name:string}', }, synonyms: { - "en-us": "Synonyms: {names:string}", - "de-ch": "Synonyme: {names:string}", - "es-es": "Sinónimos: {names:string}", - "fr-fr": "Synonymes : {names:string}", - "ru-ru": "Синонимы: {names:string}", - "uk-ua": "Синоніми: {names:string}", - "pt-br": "Sinônimos: {names:string}", + 'en-us': 'Synonyms: {names:string}', + 'de-ch': 'Synonyme: {names:string}', + 'es-es': 'Sinónimos: {names:string}', + 'fr-fr': 'Synonymes : {names:string}', + 'ru-ru': 'Синонимы: {names:string}', + 'uk-ua': 'Синоніми: {names:string}', + 'pt-br': 'Sinônimos: {names:string}', }, treeViewTitle: { - "en-us": "{treeName:string} Tree", - "ru-ru": "{treeName:string} Дерево", - "es-es": "{treeName:string} Árbol", - "fr-fr": "{treeName:string} Arbre", - "uk-ua": "{treeName:string} Дерево", - "de-ch": "{treeName:string} Baum", - "pt-br": "{treeName:string} Árvore", + 'en-us': '{treeName:string} Tree', + 'ru-ru': '{treeName:string} Дерево', + 'es-es': '{treeName:string} Árbol', + 'fr-fr': '{treeName:string} Arbre', + 'uk-ua': '{treeName:string} Дерево', + 'de-ch': '{treeName:string} Baum', + 'pt-br': '{treeName:string} Árvore', }, searchTreePlaceholder: { - "en-us": "Search Tree", - "ru-ru": "Дерево поиска", - "es-es": "Árbol de búsqueda", - "fr-fr": "Rechercher dans l'arbre", - "uk-ua": "Дерево пошуку", - "de-ch": "Baum durchsuchen", - "pt-br": "Árvore de Pesquisa", + 'en-us': 'Search Tree', + 'ru-ru': 'Дерево поиска', + 'es-es': 'Árbol de búsqueda', + 'fr-fr': "Rechercher dans l'arbre", + 'uk-ua': 'Дерево пошуку', + 'de-ch': 'Baum durchsuchen', + 'pt-br': 'Árvore de Pesquisa', }, opened: { - "en-us": "Opened", - "ru-ru": "Открыто", - "es-es": "Abierto", - "fr-fr": "Ouvert", - "uk-ua": "Відкрито", - "de-ch": "Geöffnet", - "pt-br": "Aberto", + 'en-us': 'Opened', + 'ru-ru': 'Открыто', + 'es-es': 'Abierto', + 'fr-fr': 'Ouvert', + 'uk-ua': 'Відкрито', + 'de-ch': 'Geöffnet', + 'pt-br': 'Aberto', }, closed: { - "en-us": "Closed", - "ru-ru": "Закрыто", - "es-es": "Cerrado", - "fr-fr": "Fermé", - "uk-ua": "ЗАЧИНЕНО", - "de-ch": "Geschlossen", - "pt-br": "Fechado", + 'en-us': 'Closed', + 'ru-ru': 'Закрыто', + 'es-es': 'Cerrado', + 'fr-fr': 'Fermé', + 'uk-ua': 'ЗАЧИНЕНО', + 'de-ch': 'Geschlossen', + 'pt-br': 'Fechado', }, leafNode: { - "en-us": "Leaf Node", - "ru-ru": "Листовой узел", - "es-es": "Nodo de hoja", - "fr-fr": "Nœud feuille", - "uk-ua": "Листковий вузол", - "de-ch": "Blattknoten", - "pt-br": "Nó Folha", + 'en-us': 'Leaf Node', + 'ru-ru': 'Листовой узел', + 'es-es': 'Nodo de hoja', + 'fr-fr': 'Nœud feuille', + 'uk-ua': 'Листковий вузол', + 'de-ch': 'Blattknoten', + 'pt-br': 'Nó Folha', }, nodeStats: { comment: "Used to show tree node's direct and indirect usages", - "en-us": "({directCount:number|formatted}, {childCount:number|formatted})", - "ru-ru": "({directCount:number|formatted}, {childCount:number|formatted})", - "es-es": "({directCount:number|formatted}, {childCount:number|formatted})", - "fr-fr": "({directCount:number|formatted}, {childCount:number|formatted})", - "uk-ua": "({directCount:number|formatted}', {childCount:number|formatted})", - "de-ch": "({directCount:number|formatted}, {childCount:number|formatted})", - "pt-br": "({directCount:number|formatted}, {childCount:number|formatted})", + 'en-us': '({directCount:number|formatted}, {childCount:number|formatted})', + 'ru-ru': '({directCount:number|formatted}, {childCount:number|formatted})', + 'es-es': '({directCount:number|formatted}, {childCount:number|formatted})', + 'fr-fr': '({directCount:number|formatted}, {childCount:number|formatted})', + 'uk-ua': "({directCount:number|formatted}', {childCount:number|formatted})", + 'de-ch': '({directCount:number|formatted}, {childCount:number|formatted})', + 'pt-br': '({directCount:number|formatted}, {childCount:number|formatted})', }, leafNodeStats: { comment: "Used to show leaf tree node's direct usages", - "en-us": "({directCount:number|formatted})", - "ru-ru": "({directCount:number|formatted})", - "es-es": "({directCount:number|formatted})", - "fr-fr": "({directCount:number|formatted})", - "uk-ua": "({directCount:number|formatted})'", - "de-ch": "({directCount:number|formatted})", - "pt-br": "({directCount:number|formatted})", + 'en-us': '({directCount:number|formatted})', + 'ru-ru': '({directCount:number|formatted})', + 'es-es': '({directCount:number|formatted})', + 'fr-fr': '({directCount:number|formatted})', + 'uk-ua': "({directCount:number|formatted})'", + 'de-ch': '({directCount:number|formatted})', + 'pt-br': '({directCount:number|formatted})', }, directCollectionObjectCount: { - comment: "Example: Direct Collection Object count", - "en-us": "Direct {collectionObjectTable:string} Count", - "ru-ru": "Прямой {collectionObjectTable:string} счет", - "es-es": "Recuento directo {collectionObjectTable:string}", - "de-ch": "Direkte {collectionObjectTable:string} Anzahl", - "fr-fr": "Compte direct {collectionObjectTable:string}", - "uk-ua": "Прямий підрахунок {collectionObjectTable:string}", - "pt-br": "Contagem direta {collectionObjectTable:string}", + comment: 'Example: Direct Collection Object count', + 'en-us': 'Direct {collectionObjectTable:string} Count', + 'ru-ru': 'Прямой {collectionObjectTable:string} счет', + 'es-es': 'Recuento directo {collectionObjectTable:string}', + 'de-ch': 'Direkte {collectionObjectTable:string} Anzahl', + 'fr-fr': 'Compte direct {collectionObjectTable:string}', + 'uk-ua': 'Прямий підрахунок {collectionObjectTable:string}', + 'pt-br': 'Contagem direta {collectionObjectTable:string}', }, indirectCollectionObjectCount: { - comment: "Example: Indirect Collection Object count", - "en-us": "Indirect {collectionObjectTable:string} Count", - "ru-ru": "Косвенный {collectionObjectTable:string} счет", - "es-es": "Recuento indirecto {collectionObjectTable:string}", - "fr-fr": "Compte indirect {collectionObjectTable:string}", - "uk-ua": "Непрямий підрахунок {collectionObjectTable:string}", - "de-ch": "Indirekte {collectionObjectTable:string} Anzahl", - "pt-br": "Contagem indireta {collectionObjectTable:string}", + comment: 'Example: Indirect Collection Object count', + 'en-us': 'Indirect {collectionObjectTable:string} Count', + 'ru-ru': 'Косвенный {collectionObjectTable:string} счет', + 'es-es': 'Recuento indirecto {collectionObjectTable:string}', + 'fr-fr': 'Compte indirect {collectionObjectTable:string}', + 'uk-ua': 'Непрямий підрахунок {collectionObjectTable:string}', + 'de-ch': 'Indirekte {collectionObjectTable:string} Anzahl', + 'pt-br': 'Contagem indireta {collectionObjectTable:string}', }, editRanks: { - "en-us": "Edit Ranks", - "ru-ru": "Редактировать ранги", - "es-es": "Editar rangos", - "fr-fr": "Modifier les rangs", - "uk-ua": "Редагувати ранги", - "de-ch": "Positionen bearbeiten", - "pt-br": "Editar classificações", + 'en-us': 'Edit Ranks', + 'ru-ru': 'Редактировать ранги', + 'es-es': 'Editar rangos', + 'fr-fr': 'Modifier les rangs', + 'uk-ua': 'Редагувати ранги', + 'de-ch': 'Positionen bearbeiten', + 'pt-br': 'Editar classificações', }, resourceToDelete: { - "en-us": "This will permanently delete the following resource", - "es-es": "Esto eliminará permanentemente el siguiente recurso", - "fr-fr": "Cela supprimera définitivement la ressource suivante", - "ru-ru": "Это приведет к безвозвратному удалению следующего ресурса:", - "uk-ua": "Це призведе до остаточного видалення наступного ресурсу", - "de-ch": "Dadurch wird die folgende Ressource dauerhaft gelöscht", - "pt-br": "Isso excluirá permanentemente o seguinte recurso", + 'en-us': 'This will permanently delete the following resource', + 'es-es': 'Esto eliminará permanentemente el siguiente recurso', + 'fr-fr': 'Cela supprimera définitivement la ressource suivante', + 'ru-ru': 'Это приведет к безвозвратному удалению следующего ресурса:', + 'uk-ua': 'Це призведе до остаточного видалення наступного ресурсу', + 'de-ch': 'Dadurch wird die folgende Ressource dauerhaft gelöscht', + 'pt-br': 'Isso excluirá permanentemente o seguinte recurso', }, associatedNodesOnly: { - "en-us": "Show only nodes with associated objects", - "de-ch": "Nur Knoten mit zugehörigen Objekten anzeigen", - "es-es": "Mostrar solo nodos con objetos asociados", - "fr-fr": "Afficher uniquement les nœuds avec des objets associés", - "ru-ru": "Показывать только узлы со связанными объектами", - "uk-ua": "Показувати лише вузли з пов'язаними об'єктами", - "pt-br": "Mostrar apenas nós com objetos associados", + 'en-us': 'Show only nodes with associated objects', + 'de-ch': 'Nur Knoten mit zugehörigen Objekten anzeigen', + 'es-es': 'Mostrar solo nodos con objetos asociados', + 'fr-fr': 'Afficher uniquement les nœuds avec des objets associés', + 'ru-ru': 'Показывать только узлы со связанными объектами', + 'uk-ua': "Показувати лише вузли з пов'язаними об'єктами", + 'pt-br': 'Mostrar apenas nós com objetos associados', }, splitView: { - "en-us": "Split View", - "de-ch": "Geteilte Sicht", - "es-es": "Vista dividida", - "fr-fr": "Vue fractionnée", - "ru-ru": "Разделенный вид", - "uk-ua": "Розділений вид", - "pt-br": "Visualização dividida", + 'en-us': 'Split View', + 'de-ch': 'Geteilte Sicht', + 'es-es': 'Vista dividida', + 'fr-fr': 'Vue fractionnée', + 'ru-ru': 'Разделенный вид', + 'uk-ua': 'Розділений вид', + 'pt-br': 'Visualização dividida', }, horizontal: { - "en-us": "Horizontal", - "de-ch": "Horizontal", - "es-es": "Horizontal", - "fr-fr": "Horizontal", - "ru-ru": "Горизонтальный", - "uk-ua": "Горизонтальний", - "pt-br": "Horizontal", + 'en-us': 'Horizontal', + 'de-ch': 'Horizontal', + 'es-es': 'Horizontal', + 'fr-fr': 'Horizontal', + 'ru-ru': 'Горизонтальный', + 'uk-ua': 'Горизонтальний', + 'pt-br': 'Horizontal', }, vertical: { - "en-us": "Vertical", - "de-ch": "Vertikal", - "es-es": "Vertical", - "fr-fr": "Vertical", - "ru-ru": "Вертикальный", - "uk-ua": "Вертикальний", - "pt-br": "Vertical", + 'en-us': 'Vertical', + 'de-ch': 'Vertikal', + 'es-es': 'Vertical', + 'fr-fr': 'Vertical', + 'ru-ru': 'Вертикальный', + 'uk-ua': 'Вертикальний', + 'pt-br': 'Vertical', }, synchronize: { - "en-us": "Synchronize", - "de-ch": "Synchronisieren", - "es-es": "Sincronizar", - "fr-fr": "Synchroniser", - "uk-ua": "Синхронізувати", - "ru-ru": "Синхронизировать", - "pt-br": "Sincronizar", + 'en-us': 'Synchronize', + 'de-ch': 'Synchronisieren', + 'es-es': 'Sincronizar', + 'fr-fr': 'Synchroniser', + 'uk-ua': 'Синхронізувати', + 'ru-ru': 'Синхронизировать', + 'pt-br': 'Sincronizar', }, addNewRank: { - "en-us": "Add New Rank", - "de-ch": "Neuen Rang hinzufügen", - "es-es": "Añadir nuevo rango", - "fr-fr": "Ajouter un nouveau rang", - "ru-ru": "Добавить новый ранг", - "uk-ua": "Додати новий ранг", - "pt-br": "Adicionar nova classificação", + 'en-us': 'Add New Rank', + 'de-ch': 'Neuen Rang hinzufügen', + 'es-es': 'Añadir nuevo rango', + 'fr-fr': 'Ajouter un nouveau rang', + 'ru-ru': 'Добавить новый ранг', + 'uk-ua': 'Додати новий ранг', + 'pt-br': 'Adicionar nova classificação', }, chooseParentRank: { - "en-us": "Choose Parent Rank", - "de-ch": "Wählen Sie den übergeordneten Rang", - "es-es": "Elija el rango de los padres", - "fr-fr": "Choisissez le rang des parents", - "ru-ru": "Выберите родительский ранг", - "uk-ua": "Виберіть батьківський рейтинг", - "pt-br": "Escolha a classificação dos pais", + 'en-us': 'Choose Parent Rank', + 'de-ch': 'Wählen Sie den übergeordneten Rang', + 'es-es': 'Elija el rango de los padres', + 'fr-fr': 'Choisissez le rang des parents', + 'ru-ru': 'Выберите родительский ранг', + 'uk-ua': 'Виберіть батьківський рейтинг', + 'pt-br': 'Escolha a classificação dos pais', }, moveItems: { - "en-us": "Move Items", - "de-ch": "Elemente verschieben", - "es-es": "Mover elementos", - "fr-fr": "Déplacer des éléments", - "ru-ru": "Переместить элементы", - "uk-ua": "Переміщення елементів", - "pt-br": "Mover itens", + 'en-us': 'Move Items', + 'de-ch': 'Elemente verschieben', + 'es-es': 'Mover elementos', + 'fr-fr': 'Déplacer des éléments', + 'ru-ru': 'Переместить элементы', + 'uk-ua': 'Переміщення елементів', + 'pt-br': 'Mover itens', }, addTree: { - "en-us": "Add Tree", - "de-ch": "Baum hinzufügen", - "es-es": "Agregar árbol", - "fr-fr": "Ajouter un arbre", - "pt-br": "Adicionar árvore", - "ru-ru": "Добавить дерево", - "uk-ua": "Додати дерево", + 'en-us': 'Add Tree', + 'de-ch': 'Baum hinzufügen', + 'es-es': 'Agregar árbol', + 'fr-fr': 'Ajouter un arbre', + 'pt-br': 'Adicionar árvore', + 'ru-ru': 'Добавить дерево', + 'uk-ua': 'Додати дерево', }, addRootNode: { - "en-us": "Add root node", - "de-ch": "Stammknoten hinzufügen", - "es-es": "Agregar nodo raíz", - "fr-fr": "Ajouter un nœud racine", - "pt-br": "Adicionar nó raiz", - "ru-ru": "Добавить корневой узел", - "uk-ua": "Додати кореневий вузол", + 'en-us': 'Add root node', + 'de-ch': 'Stammknoten hinzufügen', + 'es-es': 'Agregar nodo raíz', + 'fr-fr': 'Ajouter un nœud racine', + 'pt-br': 'Adicionar nó raiz', + 'ru-ru': 'Добавить корневой узел', + 'uk-ua': 'Додати кореневий вузол', }, treePicker: { - "en-us": "Tree Picker", - "de-ch": "Baumpflücker", - "es-es": "Selector de árboles", - "fr-fr": "Cueilleur d'arbres", - "pt-br": "Colhedor de árvores", - "ru-ru": "Сборщик деревьев", - "uk-ua": "Збирач дерев", + 'en-us': 'Tree Picker', + 'de-ch': 'Baumpflücker', + 'es-es': 'Selector de árboles', + 'fr-fr': "Cueilleur d'arbres", + 'pt-br': 'Colhedor de árvores', + 'ru-ru': 'Сборщик деревьев', + 'uk-ua': 'Збирач дерев', }, botany: { - "en-us": "Botany", - "de-ch": "Botanik", - "es-es": "Botánica", - "fr-fr": "Botanique", - "pt-br": "Botânica", - "ru-ru": "Ботаника", - "uk-ua": "Ботаніка", + 'en-us': 'Botany', + 'de-ch': 'Botanik', + 'es-es': 'Botánica', + 'fr-fr': 'Botanique', + 'pt-br': 'Botânica', + 'ru-ru': 'Ботаника', + 'uk-ua': 'Ботаніка', }, entomology: { - "en-us": "Entomology", - "de-ch": "Entomologie", - "es-es": "Entomología", - "fr-fr": "Entomologie", - "pt-br": "Entomologia", - "ru-ru": "Энтомология", - "uk-ua": "Ентомологія", + 'en-us': 'Entomology', + 'de-ch': 'Entomologie', + 'es-es': 'Entomología', + 'fr-fr': 'Entomologie', + 'pt-br': 'Entomologia', + 'ru-ru': 'Энтомология', + 'uk-ua': 'Ентомологія', }, herpetology: { - "en-us": "Herpetology", - "de-ch": "Herpetologie", - "es-es": "Herpetología", - "fr-fr": "Herpétologie", - "pt-br": "Herpetologia", - "ru-ru": "Герпетология", - "uk-ua": "Герпетологія", + 'en-us': 'Herpetology', + 'de-ch': 'Herpetologie', + 'es-es': 'Herpetología', + 'fr-fr': 'Herpétologie', + 'pt-br': 'Herpetologia', + 'ru-ru': 'Герпетология', + 'uk-ua': 'Герпетологія', }, ichthyology: { - "en-us": "Ichthyology", - "de-ch": "Fischkunde", - "es-es": "Ictiología", - "fr-fr": "Ichtyologie", - "pt-br": "Ictiologia", - "ru-ru": "Ихтиология", - "uk-ua": "Іхтіологія", + 'en-us': 'Ichthyology', + 'de-ch': 'Fischkunde', + 'es-es': 'Ictiología', + 'fr-fr': 'Ichtyologie', + 'pt-br': 'Ictiologia', + 'ru-ru': 'Ихтиология', + 'uk-ua': 'Іхтіологія', }, invertpaleo: { - "en-us": "Invertebrate Paleontology", - "de-ch": "Paläontologie der Wirbellosen", - "es-es": "Paleontología de invertebrados", - "fr-fr": "Paléontologie des invertébrés", - "pt-br": "Paleontologia de Invertebrados", - "ru-ru": "Палеонтология беспозвоночных", - "uk-ua": "Палеонтологія безхребетних", + 'en-us': 'Invertebrate Paleontology', + 'de-ch': 'Paläontologie der Wirbellosen', + 'es-es': 'Paleontología de invertebrados', + 'fr-fr': 'Paléontologie des invertébrés', + 'pt-br': 'Paleontologia de Invertebrados', + 'ru-ru': 'Палеонтология беспозвоночных', + 'uk-ua': 'Палеонтологія безхребетних', }, invertzoo: { - "en-us": "Invertebrate Zoology", - "de-ch": "Wirbellose Zoologie", - "es-es": "Zoología de invertebrados", - "fr-fr": "Zoologie des invertébrés", - "pt-br": "Zoologia de Invertebrados", - "ru-ru": "Зоология беспозвоночных", - "uk-ua": "Зоологія безхребетних", + 'en-us': 'Invertebrate Zoology', + 'de-ch': 'Wirbellose Zoologie', + 'es-es': 'Zoología de invertebrados', + 'fr-fr': 'Zoologie des invertébrés', + 'pt-br': 'Zoologia de Invertebrados', + 'ru-ru': 'Зоология беспозвоночных', + 'uk-ua': 'Зоологія безхребетних', }, mammalogy: { - "en-us": "Mammalogy", - "de-ch": "Mammalogie", - "es-es": "Mastozoología", - "fr-fr": "Mammalogie", - "pt-br": "Mammalogia", - "ru-ru": "Маммология", - "uk-ua": "Мамалогія", + 'en-us': 'Mammalogy', + 'de-ch': 'Mammalogie', + 'es-es': 'Mastozoología', + 'fr-fr': 'Mammalogie', + 'pt-br': 'Mammalogia', + 'ru-ru': 'Маммология', + 'uk-ua': 'Мамалогія', }, ornithology: { - "en-us": "Ornithology", - "de-ch": "Vogelkunde", - "es-es": "Ornitología", - "fr-fr": "Ornithologie", - "pt-br": "Ornitologia", - "ru-ru": "Орнитология", - "uk-ua": "Орнітологія", + 'en-us': 'Ornithology', + 'de-ch': 'Vogelkunde', + 'es-es': 'Ornitología', + 'fr-fr': 'Ornithologie', + 'pt-br': 'Ornitologia', + 'ru-ru': 'Орнитология', + 'uk-ua': 'Орнітологія', }, paleobot: { - "en-us": "Paleobotany", - "de-ch": "Paläobotanik", - "es-es": "Paleobotánica", - "fr-fr": "Paléobotanique", - "pt-br": "Paleobotânica", - "ru-ru": "Палеоботаника", - "uk-ua": "Палеоботаніка", + 'en-us': 'Paleobotany', + 'de-ch': 'Paläobotanik', + 'es-es': 'Paleobotánica', + 'fr-fr': 'Paléobotanique', + 'pt-br': 'Paleobotânica', + 'ru-ru': 'Палеоботаника', + 'uk-ua': 'Палеоботаніка', }, vascplant: { - "en-us": "Vascular Plants", - "de-ch": "Gefäßpflanzen", - "es-es": "Plantas vasculares", - "fr-fr": "Plantes vasculaires", - "pt-br": "Plantas Vasculares", - "ru-ru": "Сосудистые растения", - "uk-ua": "Судинні рослини", + 'en-us': 'Vascular Plants', + 'de-ch': 'Gefäßpflanzen', + 'es-es': 'Plantas vasculares', + 'fr-fr': 'Plantes vasculaires', + 'pt-br': 'Plantas Vasculares', + 'ru-ru': 'Сосудистые растения', + 'uk-ua': 'Судинні рослини', }, vertpaleo: { - "en-us": "Vertebrate Paleontology", - "de-ch": "Wirbeltierpaläontologie", - "es-es": "Paleontología de vertebrados", - "fr-fr": "Paléontologie des vertébrés", - "pt-br": "Paleontologia de Vertebrados", - "ru-ru": "Палеонтология позвоночных", - "uk-ua": "Палеонтологія хребетних", + 'en-us': 'Vertebrate Paleontology', + 'de-ch': 'Wirbeltierpaläontologie', + 'es-es': 'Paleontología de vertebrados', + 'fr-fr': 'Paléontologie des vertébrés', + 'pt-br': 'Paleontologia de Vertebrados', + 'ru-ru': 'Палеонтология позвоночных', + 'uk-ua': 'Палеонтологія хребетних', }, defaultRemarks: { - "en-us": "A default taxon tree", - "de-ch": "Ein Standard-Taxonbaum", - "es-es": "Un árbol de taxones predeterminado", - "fr-fr": "Un arbre taxonomique par défaut", - "pt-br": "Uma árvore de táxons padrão", - "ru-ru": "Дерево таксона по умолчанию", - "uk-ua": "Дерево таксонів за замовчуванням", + 'en-us': 'A default taxon tree', + 'de-ch': 'Ein Standard-Taxonbaum', + 'es-es': 'Un árbol de taxones predeterminado', + 'fr-fr': 'Un arbre taxonomique par défaut', + 'pt-br': 'Uma árvore de táxons padrão', + 'ru-ru': 'Дерево таксона по умолчанию', + 'uk-ua': 'Дерево таксонів за замовчуванням', }, emptyTree: { - "en-us": "Empty Tree", - "de-ch": "Leerer Baum", - "es-es": "Árbol vacío", - "fr-fr": "Arbre vide", - "pt-br": "Árvore vazia", - "ru-ru": "Пустое дерево", - "uk-ua": "Порожнє дерево", + 'en-us': 'Empty Tree', + 'de-ch': 'Leerer Baum', + 'es-es': 'Árbol vacío', + 'fr-fr': 'Arbre vide', + 'pt-br': 'Árvore vazia', + 'ru-ru': 'Пустое дерево', + 'uk-ua': 'Порожнє дерево', }, minerals: { - "en-us": "Minerals", - "de-ch": "Mineralien", - "es-es": "Minerales", - "fr-fr": "Minéraux", - "pt-br": "Minerais", - "ru-ru": "Минералы", - "uk-ua": "Мінерали", + 'en-us': 'Minerals', + 'de-ch': 'Mineralien', + 'es-es': 'Minerales', + 'fr-fr': 'Minéraux', + 'pt-br': 'Minerais', + 'ru-ru': 'Минералы', + 'uk-ua': 'Мінерали', }, rocks: { - "en-us": "Rocks", - "de-ch": "Felsen", - "es-es": "Rocas", - "fr-fr": "Rochers", - "pt-br": "Rochas", - "ru-ru": "Скалы", - "uk-ua": "Скелі", + 'en-us': 'Rocks', + 'de-ch': 'Felsen', + 'es-es': 'Rocas', + 'fr-fr': 'Rochers', + 'pt-br': 'Rochas', + 'ru-ru': 'Скалы', + 'uk-ua': 'Скелі', }, meteorites: { - "en-us": "Meteorites", - "de-ch": "Meteoriten", - "es-es": "Meteoritos", - "fr-fr": "Météorites", - "pt-br": "Meteoritos", - "ru-ru": "Метеориты", - "uk-ua": "Метеорити", + 'en-us': 'Meteorites', + 'de-ch': 'Meteoriten', + 'es-es': 'Meteoritos', + 'fr-fr': 'Météorites', + 'pt-br': 'Meteoritos', + 'ru-ru': 'Метеориты', + 'uk-ua': 'Метеорити', }, treeManagement: { 'en-us': 'Tree Management', From d55c3cb4b47d66ea550077ca45bfeb9e010ad4ed Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 22:29:05 -0500 Subject: [PATCH 080/100] fix: add missing localization strings --- .../lib/localization/preferences.behavior.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 0e0aef8ea43..405f0fdcf4a 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -1003,6 +1003,44 @@ export const preferencesBehaviorDictionary = { 'uk-ua': 'Налаштування', 'pt-br': 'Preferências de coleção', }, + auditing: { + 'en-us': 'Auditing', + }, + formatting: { + 'en-us': 'Formatting', + }, + enableAuditLog: { + 'en-us': 'Enable Audit Log', + }, + enableAuditLogDescription: { + 'en-us': + 'Globally enables or disables the audit log feature for all collections.', + }, + logFieldLevelChanges: { + 'en-us': 'Log field-level changes', + }, + logFieldLevelChangesDescription: { + 'en-us': + 'When auditing is enabled, record changes to individual field values in addition to record creation and deletion.', + }, + fullDateFormat: { + 'en-us': 'Full date format', + }, + fullDateFormatDescription: { + 'en-us': 'Select the display format for complete dates across the application.', + }, + monthYearDateFormat: { + 'en-us': 'Month/year date format', + }, + monthYearDateFormatDescription: { + 'en-us': 'Choose how partial dates that only include a month and year should be displayed.', + }, + attachmentThumbnailSize: { + 'en-us': 'Attachment thumbnail size (px)', + }, + attachmentThumbnailSizeDescription: { + 'en-us': 'Set the pixel dimensions used when generating attachment preview thumbnails.', + }, rememberDialogSizes: { 'en-us': 'Remember dialog window sizes', 'ru-ru': 'Запомните размеры диалоговых окон', From 1fac628f2a6c1f5a1365e8cf6d9d330c1c5f0d2c Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:10:30 -0500 Subject: [PATCH 081/100] feat(preferences): match collection preferences visual --- .../lib/components/Preferences/Aside.tsx | 17 +- .../Preferences/BasePreferences.tsx | 45 +- .../Preferences/globalPreferencesLoader.ts | 36 ++ .../lib/components/Preferences/index.tsx | 403 +----------------- 4 files changed, 99 insertions(+), 402 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx index 2e4e6cd813e..0917f744614 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx @@ -21,6 +21,12 @@ export function PreferencesAside({ readonly references: React.RefObject>; readonly prefType?: PreferenceType; }): JSX.Element { + const preferenceRoutes: Record = { + user: '/specify/user-preferences/', + collection: '/specify/collection-preferences/', + global: '/specify/global-preferences/', + }; + const preferencesPath = preferenceRoutes[prefType]; const definitions = usePrefDefinitions(prefType); const navigate = useNavigate(); const location = useLocation(); @@ -30,13 +36,10 @@ export function PreferencesAside({ () => isInOverlay || activeCategory === undefined ? undefined - : navigate( - `/specify/user-preferences/#${definitions[activeCategory][0]}`, - { - replace: true, - } - ), - [isInOverlay, definitions, activeCategory] + : navigate(`${preferencesPath}#${definitions[activeCategory][0]}`, { + replace: true, + }), + [isInOverlay, definitions, activeCategory, preferencesPath] ); const [freezeCategory, setFreezeCategory] = useFrozenCategory(); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/BasePreferences.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/BasePreferences.tsx index f8621cfce84..87317aa55a5 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/BasePreferences.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/BasePreferences.tsx @@ -94,15 +94,16 @@ export class BasePreferences { if (typeof this.resourcePromise === 'object') return this.resourcePromise; const { values, defaultValues } = this.options; + const isAppResourceEndpoint = values.fetchUrl.includes('app.resource'); - const valuesResource = fetchResourceId( - values.fetchUrl, - values.resourceName - ).then(async (appResourceId) => - typeof appResourceId === 'number' - ? fetchResourceData(values.fetchUrl, appResourceId) - : createDataResource(values.fetchUrl, values.resourceName) - ); + const valuesResource = isAppResourceEndpoint + ? fetchGlobalResource(values.fetchUrl, values.resourceName) + : fetchResourceId(values.fetchUrl, values.resourceName).then( + async (appResourceId) => + typeof appResourceId === 'number' + ? fetchResourceData(values.fetchUrl, appResourceId) + : createDataResource(values.fetchUrl, values.resourceName) + ); const defaultValuesResource = defaultValues === undefined @@ -425,6 +426,8 @@ const mimeType = 'application/json'; /** * Fetch ID of app resource containing preferences */ +const appResourceMimeType = 'text/plain'; + export const fetchResourceId = async ( fetchUrl: string, resourceName: string @@ -449,6 +452,32 @@ const fetchResourceData = async ( headers: { Accept: mimeType }, }).then(({ data }) => data); +const fetchGlobalResource = async ( + fetchUrl: string, + resourceName: string +): Promise => { + const url = cacheableUrl( + formatUrl(fetchUrl, { + name: resourceName, + quiet: '', + }) + ); + const { data, status, response } = await ajax(url, { + headers: { Accept: appResourceMimeType }, + expectedErrors: [Http.NO_CONTENT], + }); + + const parsedId = f.parseInt(response.headers.get('X-Record-ID') ?? undefined); + + return { + id: parsedId ?? -1, + data: status === Http.OK ? data : '', + metadata: null, + mimetype: response.headers.get('Content-Type') ?? appResourceMimeType, + name: resourceName, + }; +}; + /** * Fetch default values overrides, if exist */ diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts new file mode 100644 index 00000000000..7472fe1ea82 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -0,0 +1,36 @@ +import { ajax } from '../../utils/ajax'; +import { Http } from '../../utils/ajax/definitions'; +import { formatUrl } from '../Router/queryString'; +import { contextUnlockedPromise, foreverFetch } from '../InitialContext'; +import { type PartialPreferences } from './BasePreferences'; +import { globalPreferenceDefinitions } from './GlobalDefinitions'; +import { globalPreferences } from './globalPreferences'; +import { parseGlobalPreferences } from './globalPreferencesUtils'; + +export const loadGlobalPreferences = async (): Promise => { + const entryPoint = await contextUnlockedPromise; + if (entryPoint !== 'main') { + await foreverFetch(); + return; + } + + const { data, status } = await ajax( + formatUrl('/context/app.resource', { + name: 'GlobalPreferences', + quiet: '', + }), + { + headers: { Accept: 'text/plain' }, + expectedErrors: [Http.NO_CONTENT], + errorMode: 'visible', + } + ); + + const { raw } = parseGlobalPreferences( + status === Http.NO_CONTENT ? null : data + ); + + globalPreferences.setRaw( + raw as PartialPreferences + ); +}; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index dde00f0c0ce..ab09dbbfe62 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -16,7 +16,6 @@ import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; import { Container, H2, Key } from '../Atoms'; -import { DataEntry } from '../Atoms/DataEntry'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { Form } from '../Atoms/Form'; @@ -24,9 +23,6 @@ import { Link } from '../Atoms/Link'; import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; -import { AppResourceEditor } from '../AppResources/Editor'; -import { getScope, globalResourceKey } from '../AppResources/tree'; -import type { ScopedAppResourceDir } from '../AppResources/types'; import { hasPermission } from '../Permissions/helpers'; import { ProtectedTool } from '../Permissions/PermissionDenied'; import { PreferencesAside } from './Aside'; @@ -35,23 +31,13 @@ import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { collectionPreferences } from './collectionPreferences'; import { globalPreferences } from './globalPreferences'; +import { loadGlobalPreferences } from './globalPreferencesLoader'; import { useDarkMode } from './Hooks'; import { DefaultPreferenceItemRender } from './Renderers'; import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; -import { formatUrl } from '../Router/queryString'; -import { fetchCollection } from '../DataModel/collection'; -import { fetchResource, strictIdFromUrl } from '../DataModel/resource'; -import { serializeResource } from '../DataModel/serializers'; -import type { - SpAppResource, - SpAppResourceDir, - SpViewSetObj, -} from '../DataModel/types'; -import type { SerializedResource } from '../DataModel/helperTypes'; -import { userTypes } from '../PickLists/definitions'; export type PreferenceType = keyof typeof preferenceInstances; @@ -95,18 +81,20 @@ type DocumentHrefResolver = ) => string | undefined) | undefined; -const resolveGlobalDocumentHref = (): undefined => undefined; - const documentHrefResolvers: IR = { user: undefined, collection: undefined, - global: resolveGlobalDocumentHref, + global: undefined, }; const collectionPreferencesPromise = Promise.all([ collectionPreferences.fetch(), ]).then(f.true); +const globalPreferencesPromise = Promise.all([ + loadGlobalPreferences(), +]).then(f.true); + /** * Fetch app resource that stores current user preferences * @@ -133,7 +121,9 @@ function Preferences({ const heading = prefType === 'collection' ? preferencesText.collectionPreferences() - : preferencesText.preferences(); + : prefType === 'global' + ? resourcesText.globalPreferences() + : preferencesText.preferences(); React.useEffect( () => @@ -581,380 +571,19 @@ export function CollectionPreferencesWrapper(): JSX.Element | null { ); } -export function GlobalPreferencesWrapper(): JSX.Element { - return ; +export function GlobalPreferencesWrapper(): JSX.Element | null { + return ( + + + + ); } function GlobalPreferences(): JSX.Element { return ( - + ); } -type ResourceWithData = { - readonly id: number; - readonly data: string | null; - readonly name: string; - readonly mimetype: string | null; - readonly metadata: string | null; -}; - -type LoadedGlobalPreferences = { - readonly resource: SerializedResource; - readonly directory: ScopedAppResourceDir; - readonly data: ResourceWithData; -}; - -type LoadedCollectionPreferences = { - readonly resource: SerializedResource; - readonly directory: ScopedAppResourceDir; - readonly data: ResourceWithData; -}; - -const isAppResource = ( - resource: SerializedResource -): resource is SerializedResource => - resource._tableName === 'SpAppResource'; - -function GlobalPreferencesStandalone(): JSX.Element { - const navigate = useNavigate(); - const [state, setState] = React.useState( - undefined - ); - const [error, setError] = React.useState(undefined); - const isMountedRef = React.useRef(true); - - const renderStatus = React.useCallback( - (body: React.ReactNode, role?: 'alert'): JSX.Element => ( - -

{resourcesText.globalPreferences()}

-
- {body} -
-
- ), - [] - ); - - const loadPreferences = React.useCallback(async () => { - const { records: directories } = await fetchCollection('SpAppResourceDir', { - limit: 1, - domainFilter: false, - }, - { - usertype: 'Global Prefs', - }); - - const directoryRecord = directories[0]; - if (directoryRecord === undefined) - throw new Error('Global preferences directory not found'); - - const scopedDirectory: ScopedAppResourceDir = { - ...directoryRecord, - scope: getScope(directoryRecord), - }; - - const { records: resources } = await fetchCollection('SpAppResource', { - limit: 1, - domainFilter: false, - }, - { - spappresourcedir: directoryRecord.id, - name: 'preferences', - }); - - const rawResource = resources[0]; - if (rawResource === undefined) - throw new Error('Global preferences resource not found'); - - const resource: typeof rawResource = { - ...rawResource, - mimeType: rawResource.mimeType ?? 'text/x-java-properties', - }; - - const { records: dataRecords } = await fetchCollection('SpAppResourceData', { - limit: 1, - domainFilter: false, - }, - { - spappresource: resource.id, - }); - - const resourceData = dataRecords[0]; - if (resourceData === undefined) - throw new Error('Global preferences data not found'); - - return { - resource, - directory: scopedDirectory, - data: { - id: resource.id, - name: resource.name ?? 'preferences', - mimetype: resource.mimeType ?? 'text/x-java-properties', - metadata: (resource as { metaData?: string | null }).metaData ?? null, - data: resourceData.data ?? '', - }, - } as LoadedGlobalPreferences; - }, []); - - const refresh = React.useCallback(() => { - loadPreferences() - .then((loaded) => { - if (!isMountedRef.current) return; - setState(loaded); - setError(undefined); - }) - .catch((loadError) => { - if (!isMountedRef.current) return; - setError(loadError); - }); - }, [loadPreferences]); - - React.useEffect(() => { - isMountedRef.current = true; - refresh(); - return () => { - isMountedRef.current = false; - }; - }, [refresh]); - - const handleClone = React.useCallback( - ( - clonedResource: SerializedResource, - cloneId: number | undefined - ) => { - const appResourceClone = isAppResource(clonedResource) - ? clonedResource - : undefined; - if (appResourceClone === undefined) return; - const directoryKey = - state?.directory !== undefined - ? getDirectoryKey(state.directory) ?? globalResourceKey - : globalResourceKey; - navigate( - formatUrl('/specify/resources/app-resource/new/', { - directoryKey, - name: appResourceClone.name, - ...(appResourceClone.mimeType == null - ? {} - : { mimeType: appResourceClone.mimeType }), - clone: cloneId, - }) - ); - }, - [navigate, state] - ); - - if (error !== undefined && state === undefined) - return renderStatus( - 'Failed to open global preferences. Try accessing them through App Resources.', - 'alert' - ); - - if (state === undefined) return renderStatus(commonText.loading()); - - return ( - - - {({ headerJsx, headerButtons, form, footer }): JSX.Element => ( - - - {headerJsx} - {headerButtons} - - {form} - {footer} - - )} - - - ); -} - -function CollectionPreferencesStandalone(): JSX.Element { - const navigate = useNavigate(); - const [state, setState] = React.useState( - undefined - ); - const [error, setError] = React.useState(undefined); - - const renderStatus = React.useCallback( - (body: React.ReactNode, role?: 'alert'): JSX.Element => ( - -

{preferencesText.collectionPreferences()}

-
- {body} -
-
- ), - [] - ); - - React.useEffect(() => { - let isMounted = true; - const load = async () => { - try { - const rawData = (await collectionPreferences.fetch()) as ResourceWithData; - const data: ResourceWithData = { - ...rawData, - data: rawData.data ?? '', - }; - if (!isMounted) return; - const resource = await fetchResource('SpAppResource', data.id); - if (!isMounted) return; - const directory = await resolveDirectory(resource); - if (!isMounted) return; - setState({ resource, directory, data }); - } catch (loadError) { - if (!isMounted) return; - setError(loadError); - } - }; - load(); - return () => { - isMounted = false; - }; - }, []); - - const handleClone = React.useCallback( - ( - clonedResource: SerializedResource, - cloneId: number | undefined - ) => { - const appResourceClone = isAppResource(clonedResource) - ? clonedResource - : undefined; - if (appResourceClone === undefined) return; - const directoryKey = - state === undefined - ? globalResourceKey - : getDirectoryKey(state.directory) ?? globalResourceKey; - navigate( - formatUrl('/specify/resources/app-resource/new/', { - directoryKey, - name: appResourceClone.name, - ...(appResourceClone.mimeType == null - ? {} - : { mimeType: appResourceClone.mimeType }), - clone: cloneId, - }) - ); - }, - [navigate, state] - ); - - if (error !== undefined && state === undefined) - return renderStatus( - 'Failed to open collection preferences. Try accessing them through App Resources.', - 'alert' - ); - - if (state === undefined) return renderStatus(commonText.loading()); - - return ( - - { - setState((previousState) => - previousState === undefined - ? previousState - : { - resource: - updatedResource as SerializedResource, - directory: updatedDirectory, - data: previousState.data, - } - ); - collectionPreferences - .fetch() - .then((rawData) => ({ - ...rawData, - data: rawData.data ?? '', - })) - .then((updatedData) => { - setState({ - resource: updatedResource as SerializedResource, - directory: updatedDirectory, - data: updatedData as ResourceWithData, - }); - }) - .catch((fetchError) => { - if (state === undefined) setError(fetchError); - }); - }} - > - {({ headerJsx, headerButtons, form, footer }): JSX.Element => ( - - - {headerJsx} - {headerButtons} - - {form} - {footer} - - )} - - - ); -} - -async function resolveDirectory( - resource: SerializedResource -): Promise { - const rawDirectory = resource.spAppResourceDir; - let directory: SerializedResource; - if (typeof rawDirectory === 'string') { - directory = await fetchResource( - 'SpAppResourceDir', - strictIdFromUrl(rawDirectory) - ); - } else if (typeof rawDirectory === 'object' && rawDirectory !== null) { - directory = serializeResource(rawDirectory); - } else { - throw new Error('Collection preferences resource is missing directory'); - } - return { - ...directory, - scope: getScope(directory), - }; -} - -function getDirectoryKey(directory: ScopedAppResourceDir): string | undefined { - if (directory.scope === 'global') return globalResourceKey; - if (directory.scope === 'discipline' && directory.discipline !== null) - return `discipline_${strictIdFromUrl(directory.discipline)}`; - if (directory.scope === 'collection' && directory.collection !== null) - return `collection_${strictIdFromUrl(directory.collection)}`; - if ( - directory.scope === 'userType' && - directory.collection !== null && - directory.userType !== null - ) { - const userTypeLabel = - userTypes.find( - (type) => type.toLowerCase() === directory.userType?.toLowerCase() - ) ?? directory.userType; - return `collection_${strictIdFromUrl(directory.collection)}_userType_${userTypeLabel}`; - } - if ( - directory.scope === 'user' && - directory.collection !== null && - directory.specifyUser !== null - ) - return `collection_${strictIdFromUrl(directory.collection)}_user_${strictIdFromUrl(directory.specifyUser)}`; - return undefined; -} From 914d3918f21d776eae491b4c3ad24969f73aa5e4 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:19:29 -0500 Subject: [PATCH 082/100] fix: add static test globalpreferences --- .../app.resource/name=GlobalPreferences&quiet=.properties | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/tests/ajax/static/context/app.resource/name=GlobalPreferences&quiet=.properties diff --git a/specifyweb/frontend/js_src/lib/tests/ajax/static/context/app.resource/name=GlobalPreferences&quiet=.properties b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/app.resource/name=GlobalPreferences&quiet=.properties new file mode 100644 index 00000000000..8f80091aff7 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/tests/ajax/static/context/app.resource/name=GlobalPreferences&quiet=.properties @@ -0,0 +1,5 @@ +auditing.do_audits=true +auditing.audit_field_updates=true +ui.formatting.scrdateformat=YYYY-MM-DD +ui.formatting.scrmonthformat=YYYY-MM +attachment.preview_size=256 From 747f726634e49202a1679719a4d0dfb331ef593e Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Sat, 18 Oct 2025 04:23:22 +0000 Subject: [PATCH 083/100] Lint code with ESLint and Prettier Triggered by 914d3918f21d776eae491b4c3ad24969f73aa5e4 on branch refs/heads/issue-7442-3 --- .../lib/components/Preferences/index.tsx | 161 +++++++++--------- 1 file changed, 85 insertions(+), 76 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index ab09dbbfe62..7ba5ddb6693 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -58,7 +58,10 @@ type SubcategoryDocumentation = { readonly label: LocalizedString | (() => LocalizedString); }; -const SUBCATEGORY_DOCS_MAP: Record> = { +const SUBCATEGORY_DOCS_MAP: Record< + string, + Record +> = { treeManagement: { synonymized: { href: 'https://discourse.specifysoftware.org/t/enable-creating-children-for-synonymized-nodes/987', @@ -75,10 +78,10 @@ const SUBCATEGORY_DOCS_MAP: Record string | undefined) + category: string, + subcategory: string, + name: string + ) => string | undefined) | undefined; const documentHrefResolvers: IR = { @@ -91,9 +94,9 @@ const collectionPreferencesPromise = Promise.all([ collectionPreferences.fetch(), ]).then(f.true); -const globalPreferencesPromise = Promise.all([ - loadGlobalPreferences(), -]).then(f.true); +const globalPreferencesPromise = Promise.all([loadGlobalPreferences()]).then( + f.true +); /** * Fetch app resource that stores current user preferences @@ -245,7 +248,8 @@ export function PreferencesContent({ const isReadOnly = React.useContext(ReadOnlyContext); const definitions = usePrefDefinitions(prefType); const basePreferences = preferenceInstances[prefType]; - const preferences = React.useContext(basePreferences.Context) ?? basePreferences; + const preferences = + React.useContext(basePreferences.Context) ?? basePreferences; const resolveDocumentHref = documentHrefResolvers[prefType]; const definitionsMap = React.useMemo( () => new Map(definitions), @@ -325,48 +329,52 @@ export function PreferencesContent({ {typeof description === 'function' ? description() : description}

)} - {items.map(([name, item]) => { - const canEdit = - !isReadOnly && - (item.visible !== 'protected' || - hasPermission('/preferences/user', 'edit_protected')); - const documentHref = resolveDocumentHref?.( - categoryKey, - subcategoryKey, - name - ); - const stackDocumentation = - prefType === 'collection' && documentHref !== undefined; - const props = { - className: ` + {items.map(([name, item]) => { + const canEdit = + !isReadOnly && + (item.visible !== 'protected' || + hasPermission('/preferences/user', 'edit_protected')); + const documentHref = resolveDocumentHref?.( + categoryKey, + subcategoryKey, + name + ); + const stackDocumentation = + prefType === 'collection' && documentHref !== undefined; + const props = { + className: ` flex items-start gap-2 md:flex-row flex-col ${canEdit ? '' : '!cursor-not-allowed'} `, - key: name, - title: canEdit ? undefined : preferencesText.adminsOnlyPreference(), - }; - const children = ( - <> -
-

+

+

- -

- {(item.description !== undefined || - documentHref !== undefined) && ( + > + +

+ {(item.description !== undefined || + documentHref !== undefined) && (

{item.description !== undefined && ( @@ -380,7 +388,9 @@ export function PreferencesContent({ )} {documentHref !== undefined && ( {headerText.documentation()} @@ -388,40 +398,35 @@ export function PreferencesContent({ )}

)} -
-
+
- - - -
- - ); - return 'container' in item && item.container === 'div' ? ( -
{children}
- ) : ( - - ); - })} + > + + + +
+ + ); + return 'container' in item && item.container === 'div' ? ( +
{children}
+ ) : ( + + ); + })} ); }, - [ - isReadOnly, - prefType, - preferences, - resolveDocumentHref, - ] + [isReadOnly, prefType, preferences, resolveDocumentHref] ); return ( @@ -431,13 +436,18 @@ export function PreferencesContent({ [category, { title, description = undefined, subCategories }], index ) => { - if (prefType === 'collection' && category === 'catalogNumberParentInheritance') + if ( + prefType === 'collection' && + category === 'catalogNumberParentInheritance' + ) return null; const isCatalogInheritance = - prefType === 'collection' && category === 'catalogNumberInheritance'; + prefType === 'collection' && + category === 'catalogNumberInheritance'; const parentDefinition = isCatalogInheritance - ? definitionsMap.get('catalogNumberParentInheritance') ?? undefined + ? (definitionsMap.get('catalogNumberParentInheritance') ?? + undefined) : undefined; return ( @@ -586,4 +596,3 @@ function GlobalPreferences(): JSX.Element { ); } - From 98f7152b5e00f198e5c419c3c45a3cdaec7e2de2 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:00:36 -0500 Subject: [PATCH 084/100] feat: organize global settings into categories --- .../Preferences/GlobalDefinitions.ts | 81 ++++++++++++------- .../Preferences/globalPreferences.ts | 6 +- .../Preferences/globalPreferencesUtils.ts | 58 +++++++------ .../lib/localization/preferences.behavior.ts | 6 +- 4 files changed, 94 insertions(+), 57 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index c21e92a93ce..9b6de5a9ced 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -2,44 +2,34 @@ import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { localized } from '../../utils/types'; import { definePref } from './types'; +import type { GenericPreferences } from './types'; export const FULL_DATE_FORMAT_OPTIONS = [ - 'YYYY-MM-DD', - 'MM/DD/YYYY', - 'DD/MM/YYYY', - 'YYYY/MM/DD', - 'DD MMM YYYY', + 'yyyy-MM-dd', + 'yyyy MM dd', + 'yyyy.MM.dd', + 'yyyy/MM/dd', + 'MM dd yyyy', + 'MM-dd-yyyy', + 'MM.dd.yyyy', + 'MM/dd/yyyy', + 'dd MM yyyy', + 'dd MMM yyyy', + 'dd-MM-yyyy', + 'dd-MMM-yyyy', + 'dd.MM.yyyy', + 'dd.MMM.yyyy', + 'dd/MM/yyyy', ] as const; export const MONTH_YEAR_FORMAT_OPTIONS = ['YYYY-MM', 'MM/YYYY', 'YYYY/MM'] as const; export const globalPreferenceDefinitions = { - general: { - title: preferencesText.general(), +formatting: { + title: preferencesText.formatting, subCategories: { - auditing: { - title: preferencesText.auditing(), - items: { - enableAuditLog: definePref({ - title: preferencesText.enableAuditLog(), - description: preferencesText.enableAuditLogDescription(), - requiresReload: false, - visible: true, - defaultValue: true, - type: 'java.lang.Boolean', - }), - logFieldLevelChanges: definePref({ - title: preferencesText.logFieldLevelChanges(), - description: preferencesText.logFieldLevelChangesDescription(), - requiresReload: false, - visible: true, - defaultValue: true, - type: 'java.lang.Boolean', - }), - }, - }, formatting: { - title: preferencesText.formatting(), + title: preferencesText.general(), items: { fullDateFormat: definePref({ title: preferencesText.fullDateFormat(), @@ -65,8 +55,39 @@ export const globalPreferenceDefinitions = { }), }, }, + }, + }, + auditing: { + title: preferencesText.auditing(), + subCategories: { + auditing: { + title: preferencesText.general(), + items: { + enableAuditLog: definePref({ + title: preferencesText.enableAuditLog(), + description: preferencesText.enableAuditLogDescription(), + requiresReload: false, + visible: true, + defaultValue: true, + type: 'java.lang.Boolean', + }), + logFieldLevelChanges: definePref({ + title: preferencesText.logFieldLevelChanges(), + description: preferencesText.logFieldLevelChangesDescription(), + requiresReload: false, + visible: true, + defaultValue: true, + type: 'java.lang.Boolean', + }), + }, + }, + }, + }, + attachments: { + title: attachmentsText.attachments, + subCategories: { attachments: { - title: attachmentsText.attachments(), + title: preferencesText.general(), items: { attachmentThumbnailSize: definePref({ title: preferencesText.attachmentThumbnailSize(), diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts index fb796f707d8..00577e5d863 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferences.ts @@ -2,15 +2,19 @@ import { BasePreferences } from './BasePreferences'; import { globalPreferenceDefinitions } from './GlobalDefinitions'; export type GlobalPreferenceValues = { - readonly general: { + readonly auditing: { readonly auditing: { readonly enableAuditLog: boolean; readonly logFieldLevelChanges: boolean; }; + }; + readonly formatting: { readonly formatting: { readonly fullDateFormat: string; readonly monthYearDateFormat: string; }; + }; + readonly attachments: { readonly attachments: { readonly attachmentThumbnailSize: number; }; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 528088a53e9..730d7650d49 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -30,15 +30,19 @@ const DATE_FORMAT_NORMALIZER = new Set([ ]); export const DEFAULT_VALUES: GlobalPreferenceValues = { - general: { + auditing: { auditing: { enableAuditLog: true, logFieldLevelChanges: true, }, + }, + formatting: { formatting: { fullDateFormat: 'YYYY-MM-DD', monthYearDateFormat: 'YYYY-MM', }, + }, + attachments: { attachments: { attachmentThumbnailSize: 256, }, @@ -90,36 +94,40 @@ function parseNumber(value: string | undefined, fallback: number): number { export function preferencesFromMap(map: Record): GlobalPreferenceValues { const fullDateFormat = normalizeFormat( - map[PREFERENCE_KEYS.fullDateFormat] ?? DEFAULT_VALUES.general.formatting.fullDateFormat + map[PREFERENCE_KEYS.fullDateFormat] ?? DEFAULT_VALUES.formatting.formatting.fullDateFormat ); const monthYearFormat = normalizeFormat( - map[PREFERENCE_KEYS.monthYearDateFormat] ?? DEFAULT_VALUES.general.formatting.monthYearDateFormat + map[PREFERENCE_KEYS.monthYearDateFormat] ?? DEFAULT_VALUES.formatting.formatting.monthYearDateFormat ); return { - general: { + auditing: { auditing: { enableAuditLog: parseBoolean( map[PREFERENCE_KEYS.enableAuditLog], - DEFAULT_VALUES.general.auditing.enableAuditLog + DEFAULT_VALUES.auditing.auditing.enableAuditLog ), logFieldLevelChanges: parseBoolean( map[PREFERENCE_KEYS.logFieldLevelChanges], - DEFAULT_VALUES.general.auditing.logFieldLevelChanges + DEFAULT_VALUES.auditing.auditing.logFieldLevelChanges ), }, + }, + formatting: { formatting: { fullDateFormat, monthYearDateFormat: MONTH_YEAR_FORMAT_OPTIONS.includes( monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number] ) ? (monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number]) - : DEFAULT_VALUES.general.formatting.monthYearDateFormat, + : DEFAULT_VALUES.formatting.formatting.monthYearDateFormat, }, + }, + attachments: { attachments: { attachmentThumbnailSize: parseNumber( map[PREFERENCE_KEYS.attachmentThumbnailSize], - DEFAULT_VALUES.general.attachments.attachmentThumbnailSize + DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize ), }, }, @@ -139,26 +147,30 @@ function normalizeValues( ): GlobalPreferenceValues { const merged = values ?? DEFAULT_VALUES; return { - general: { + auditing: { auditing: { enableAuditLog: - merged.general?.auditing?.enableAuditLog ?? DEFAULT_VALUES.general.auditing.enableAuditLog, + merged.auditing?.auditing?.enableAuditLog ?? DEFAULT_VALUES.auditing.auditing.enableAuditLog, logFieldLevelChanges: - merged.general?.auditing?.logFieldLevelChanges ?? - DEFAULT_VALUES.general.auditing.logFieldLevelChanges, + merged.auditing?.auditing?.logFieldLevelChanges ?? + DEFAULT_VALUES.auditing.auditing.logFieldLevelChanges, }, + }, + formatting: { formatting: { fullDateFormat: - merged.general?.formatting?.fullDateFormat ?? - DEFAULT_VALUES.general.formatting.fullDateFormat, + merged.formatting?.formatting?.fullDateFormat ?? + DEFAULT_VALUES.formatting.formatting.fullDateFormat, monthYearDateFormat: - merged.general?.formatting?.monthYearDateFormat ?? - DEFAULT_VALUES.general.formatting.monthYearDateFormat, + merged.formatting?.formatting?.monthYearDateFormat ?? + DEFAULT_VALUES.formatting.formatting.monthYearDateFormat, }, + }, + attachments: { attachments: { attachmentThumbnailSize: - merged.general?.attachments?.attachmentThumbnailSize ?? - DEFAULT_VALUES.general.attachments.attachmentThumbnailSize, + merged.attachments?.attachments?.attachmentThumbnailSize ?? + DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize, }, }, }; @@ -166,15 +178,15 @@ function normalizeValues( function preferencesToKeyValue(values: GlobalPreferenceValues): Record { return { - [PREFERENCE_KEYS.enableAuditLog]: values.general.auditing.enableAuditLog ? 'true' : 'false', - [PREFERENCE_KEYS.logFieldLevelChanges]: values.general.auditing.logFieldLevelChanges + [PREFERENCE_KEYS.enableAuditLog]: values.auditing.auditing.enableAuditLog ? 'true' : 'false', + [PREFERENCE_KEYS.logFieldLevelChanges]: values.auditing.auditing.logFieldLevelChanges ? 'true' : 'false', - [PREFERENCE_KEYS.fullDateFormat]: normalizeFormat(values.general.formatting.fullDateFormat), + [PREFERENCE_KEYS.fullDateFormat]: normalizeFormat(values.formatting.formatting.fullDateFormat), [PREFERENCE_KEYS.monthYearDateFormat]: normalizeFormat( - values.general.formatting.monthYearDateFormat + values.formatting.formatting.monthYearDateFormat ), - [PREFERENCE_KEYS.attachmentThumbnailSize]: values.general.attachments.attachmentThumbnailSize.toString(), + [PREFERENCE_KEYS.attachmentThumbnailSize]: values.attachments.attachments.attachmentThumbnailSize.toString(), }; } diff --git a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts index 405f0fdcf4a..9296e8b3137 100644 --- a/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts +++ b/specifyweb/frontend/js_src/lib/localization/preferences.behavior.ts @@ -1007,14 +1007,14 @@ export const preferencesBehaviorDictionary = { 'en-us': 'Auditing', }, formatting: { - 'en-us': 'Formatting', + 'en-us': 'Date Format', }, enableAuditLog: { 'en-us': 'Enable Audit Log', }, enableAuditLogDescription: { 'en-us': - 'Globally enables or disables the audit log feature for all collections.', + 'Globally enables or disables the audit log for all collections.', }, logFieldLevelChanges: { 'en-us': 'Log field-level changes', @@ -1027,7 +1027,7 @@ export const preferencesBehaviorDictionary = { 'en-us': 'Full date format', }, fullDateFormatDescription: { - 'en-us': 'Select the display format for complete dates across the application.', + 'en-us': 'This determines the date format used for full dates in the WorkBench, queries, and data exports.', }, monthYearDateFormat: { 'en-us': 'Month/year date format', From 2b35679d6d7ce030f6f761d7ec6379b6053e339e Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Sat, 18 Oct 2025 00:04:46 -0500 Subject: [PATCH 085/100] fix: failing test --- .../js_src/lib/components/Preferences/GlobalDefinitions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index 9b6de5a9ced..61d20936421 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -2,7 +2,6 @@ import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { localized } from '../../utils/types'; import { definePref } from './types'; -import type { GenericPreferences } from './types'; export const FULL_DATE_FORMAT_OPTIONS = [ 'yyyy-MM-dd', From 5cc807806c96919ea7830fdd3b35e83c413374ea Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 20 Oct 2025 13:11:33 -0400 Subject: [PATCH 086/100] fixing failing tests --- .../js_src/lib/components/Preferences/GlobalDefinitions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index 61d20936421..31ee461dbfd 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -25,7 +25,7 @@ export const MONTH_YEAR_FORMAT_OPTIONS = ['YYYY-MM', 'MM/YYYY', 'YYYY/MM'] as co export const globalPreferenceDefinitions = { formatting: { - title: preferencesText.formatting, + title: preferencesText.formatting(), subCategories: { formatting: { title: preferencesText.general(), @@ -83,7 +83,7 @@ formatting: { }, }, attachments: { - title: attachmentsText.attachments, + title: attachmentsText.attachments(), subCategories: { attachments: { title: preferencesText.general(), From 8f7c0aea8908644d2a4a3938b7539518aec2459a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 20 Oct 2025 13:21:53 -0400 Subject: [PATCH 087/100] Migrate screen date format from remote prefs into GlobalPreferences --- specifyweb/backend/context/remote_prefs.py | 7 +- specifyweb/backend/stored_queries/format.py | 12 +- .../components/InitialContext/remotePrefs.ts | 13 +- .../__tests__/globalPreferencesUtils.test.ts | 51 ++++++ .../Preferences/globalPreferencesLoader.ts | 110 +++++++++++- .../Preferences/globalPreferencesUtils.ts | 165 +++++++++++------- 6 files changed, 287 insertions(+), 71 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts diff --git a/specifyweb/backend/context/remote_prefs.py b/specifyweb/backend/context/remote_prefs.py index ab77a7e8170..6af035f8ee7 100644 --- a/specifyweb/backend/context/remote_prefs.py +++ b/specifyweb/backend/context/remote_prefs.py @@ -15,6 +15,11 @@ def get_remote_prefs() -> str: def get_global_prefs() -> str: res = Spappresourcedata.objects.filter( + spappresource__name='GlobalPreferences') + if res.exists(): + return '\n'.join(force_str(r.data) for r in res) + + legacy_res = Spappresourcedata.objects.filter( spappresource__name='preferences', spappresource__spappresourcedir__usertype='Global Prefs') - return '\n'.join(force_str(r.data) for r in res) + return '\n'.join(force_str(r.data) for r in legacy_res) diff --git a/specifyweb/backend/stored_queries/format.py b/specifyweb/backend/stored_queries/format.py index 9a06926678c..5e985ec8af9 100644 --- a/specifyweb/backend/stored_queries/format.py +++ b/specifyweb/backend/stored_queries/format.py @@ -18,7 +18,7 @@ from sqlalchemy import types import specifyweb.backend.context.app_resource as app_resource -from specifyweb.backend.context.remote_prefs import get_remote_prefs +from specifyweb.backend.context.remote_prefs import get_global_prefs, get_remote_prefs from specifyweb.specify.utils.agent_types import agent_types from specifyweb.specify.models import datamodel, Splocalecontainer @@ -439,8 +439,14 @@ def _fieldformat(self, table: Table, specify_field: Field, def get_date_format() -> str: - match = re.search(r'ui\.formatting\.scrdateformat=(.+)', get_remote_prefs()) - date_format = match.group(1).strip() if match is not None else 'yyyy-MM-dd' + date_format_text = 'yyyy-MM-dd' + for prefs in (get_global_prefs(), get_remote_prefs()): + match = re.search(r'ui\.formatting\.scrdateformat=(.+)', prefs) + if match is not None: + date_format_text = match.group(1).strip() + break + + date_format = date_format_text mysql_date_format = LDLM_TO_MYSQL.get(date_format, "%Y-%m-%d") logger.debug("dateformat = %s = %s", date_format, mysql_date_format) return mysql_date_format diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts index 5da79de6724..c4aa1e1c481 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts @@ -12,6 +12,11 @@ import type { IR, R, RA } from '../../utils/types'; import { defined } from '../../utils/types'; import type { JavaType } from '../DataModel/specifyField'; import { cacheableUrl, contextUnlockedPromise } from './index'; +import { + mergeWithDefaultValues, + partialPreferencesFromMap, + setGlobalPreferenceFallback, +} from '../Preferences/globalPreferencesUtils'; const preferences: R = {}; @@ -35,7 +40,13 @@ export const fetchContext = contextUnlockedPromise.then(async (entrypoint) => preferences[key.trim()] = value.trim(); }) ) - .then(() => preferences) + .then(() => { + const fallback = mergeWithDefaultValues( + partialPreferencesFromMap(preferences) + ); + setGlobalPreferenceFallback(fallback); + return preferences; + }) : undefined ); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts new file mode 100644 index 00000000000..8bc37ed215f --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts @@ -0,0 +1,51 @@ +import { + DEFAULT_VALUES, + mergeWithDefaultValues, + partialPreferencesFromMap, + serializeGlobalPreferences, + setGlobalPreferenceFallback, +} from '../globalPreferencesUtils'; + +describe('globalPreferencesUtils', () => { + beforeEach(() => { + setGlobalPreferenceFallback(DEFAULT_VALUES); + }); + + it('builds partial values from remote preferences map', () => { + const partial = partialPreferencesFromMap({ + 'ui.formatting.scrdateformat': 'MM/dd/yyyy', + 'auditing.do_audits': 'false', + }); + + expect(partial.formatting?.formatting?.fullDateFormat).toBe('MM/DD/YYYY'); + expect(partial.auditing?.auditing?.enableAuditLog).toBe(false); + expect(partial.attachments).toBeUndefined(); + }); + + it('merges partial values with fallback defaults', () => { + const remotePartial = partialPreferencesFromMap({ + 'ui.formatting.scrdateformat': 'MM/dd/yyyy', + }); + + const merged = mergeWithDefaultValues(remotePartial); + + expect(merged.formatting.formatting.fullDateFormat).toBe('MM/DD/YYYY'); + expect(merged.attachments.attachments.attachmentThumbnailSize).toBe( + DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize + ); + }); + + it('serializes using configured fallback values', () => { + const fallback = mergeWithDefaultValues( + partialPreferencesFromMap({ + 'ui.formatting.scrdateformat': 'MM/dd/yyyy', + }) + ); + setGlobalPreferenceFallback(fallback); + + const { data } = serializeGlobalPreferences(undefined, [], { fallback }); + + expect(data).toContain('ui.formatting.scrdateformat=MM/DD/YYYY'); + expect(data).toContain('auditing.do_audits=true'); + }); +}); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index 7472fe1ea82..bd55b3485cd 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -1,11 +1,64 @@ import { ajax } from '../../utils/ajax'; import { Http } from '../../utils/ajax/definitions'; +import { ping } from '../../utils/ajax/ping'; import { formatUrl } from '../Router/queryString'; import { contextUnlockedPromise, foreverFetch } from '../InitialContext'; +import { fetchContext as fetchRemotePrefs, remotePrefs } from '../InitialContext/remotePrefs'; +import { keysToLowerCase } from '../../utils/utils'; import { type PartialPreferences } from './BasePreferences'; import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { globalPreferences } from './globalPreferences'; -import { parseGlobalPreferences } from './globalPreferencesUtils'; +import type { GlobalPreferenceValues } from './globalPreferences'; +import { + DEFAULT_VALUES, + mergeWithDefaultValues, + parseGlobalPreferences, + partialPreferencesFromMap, + serializeGlobalPreferences, + setGlobalPreferenceFallback, +} from './globalPreferencesUtils'; + +type PartialGlobalValues = Partial; + +const GLOBAL_RESOURCE_URL = '/context/app.resource/'; + +function mergeMissingFromRemote( + existing: PartialGlobalValues, + remote: PartialGlobalValues +): { readonly merged: PartialGlobalValues; readonly changed: boolean } { + let changed = false; + let merged = existing; + + const remoteFormatting = remote.formatting?.formatting; + if (remoteFormatting !== undefined) { + const currentFormatting = existing.formatting?.formatting ?? {}; + const updatedFormatting: Partial = { + ...currentFormatting, + }; + + (['fullDateFormat', 'monthYearDateFormat'] as const).forEach((key) => { + const remoteValue = remoteFormatting[key]; + if ( + remoteValue !== undefined && + updatedFormatting[key] === undefined && + remoteValue !== DEFAULT_VALUES.formatting.formatting[key] + ) { + updatedFormatting[key] = remoteValue; + changed = true; + } + }); + + if (changed) + merged = { + ...merged, + formatting: { + formatting: updatedFormatting as GlobalPreferenceValues['formatting']['formatting'], + }, + }; + } + + return { merged, changed }; +} export const loadGlobalPreferences = async (): Promise => { const entryPoint = await contextUnlockedPromise; @@ -14,7 +67,7 @@ export const loadGlobalPreferences = async (): Promise => { return; } - const { data, status } = await ajax( + const { data, status, response } = await ajax( formatUrl('/context/app.resource', { name: 'GlobalPreferences', quiet: '', @@ -26,11 +79,60 @@ export const loadGlobalPreferences = async (): Promise => { } ); - const { raw } = parseGlobalPreferences( + await fetchRemotePrefs.catch(() => undefined); + + const remotePartial = partialPreferencesFromMap(remotePrefs); + const remoteDefaults = mergeWithDefaultValues(remotePartial); + setGlobalPreferenceFallback(remoteDefaults); + globalPreferences.setDefaults( + remotePartial as PartialPreferences + ); + + const { raw, metadata } = parseGlobalPreferences( status === Http.NO_CONTENT ? null : data ); + const { merged: migratedRaw, changed } = mergeMissingFromRemote(raw, remotePartial); + + if (changed) { + try { + const serialized = serializeGlobalPreferences(migratedRaw, metadata, { + fallback: remoteDefaults, + }); + const originalData = status === Http.NO_CONTENT ? '' : data; + + if (serialized.data.trim() !== originalData.trim()) { + const resourceIdHeader = response.headers.get('X-Record-ID'); + const resourceId = + resourceIdHeader === null ? undefined : Number.parseInt(resourceIdHeader, 10); + const payload = keysToLowerCase({ + name: 'GlobalPreferences', + mimeType: 'text/plain', + metaData: '', + data: serialized.data, + }); + + if (typeof resourceId === 'number' && Number.isFinite(resourceId) && resourceId >= 0) + await ping(`${GLOBAL_RESOURCE_URL}${resourceId}/`, { + method: 'PUT', + body: payload, + headers: { Accept: 'application/json' }, + errorMode: 'silent', + }); + else + await ping(GLOBAL_RESOURCE_URL, { + method: 'POST', + body: payload, + headers: { Accept: 'application/json' }, + errorMode: 'silent', + }); + } + } catch (error) { + console.error('Failed migrating remote preferences into GlobalPreferences', error); + } + } + globalPreferences.setRaw( - raw as PartialPreferences + (changed ? migratedRaw : raw) as PartialPreferences ); }; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 730d7650d49..702bc335317 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -49,6 +49,18 @@ export const DEFAULT_VALUES: GlobalPreferenceValues = { }, }; +let globalPreferenceFallback: GlobalPreferenceValues = DEFAULT_VALUES; + +export function setGlobalPreferenceFallback( + fallback: GlobalPreferenceValues +): void { + globalPreferenceFallback = fallback; +} + +export function getGlobalPreferenceFallback(): GlobalPreferenceValues { + return globalPreferenceFallback; +} + function normalizeFormat(value: string): string { const upper = value.toUpperCase(); return DATE_FORMAT_NORMALIZER.has(upper) ? upper : upper; @@ -79,103 +91,127 @@ function parseProperties(data: string): ParsedProperties { return { lines: parsed, map }; } -function parseBoolean(value: string | undefined, fallback: boolean): boolean { - if (typeof value !== 'string') return fallback; +function parseBoolean(value: string | undefined): boolean | undefined { + if (typeof value !== 'string') return undefined; if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; - return fallback; + return undefined; } -function parseNumber(value: string | undefined, fallback: number): number { - if (typeof value !== 'string') return fallback; +function parseNumber(value: string | undefined): number | undefined { + if (typeof value !== 'string') return undefined; const parsed = Number.parseInt(value, 10); - return Number.isNaN(parsed) ? fallback : parsed; + return Number.isNaN(parsed) ? undefined : parsed; } -export function preferencesFromMap(map: Record): GlobalPreferenceValues { - const fullDateFormat = normalizeFormat( - map[PREFERENCE_KEYS.fullDateFormat] ?? DEFAULT_VALUES.formatting.formatting.fullDateFormat - ); - const monthYearFormat = normalizeFormat( - map[PREFERENCE_KEYS.monthYearDateFormat] ?? DEFAULT_VALUES.formatting.formatting.monthYearDateFormat +function hasProperties>(object: T): object is T { + return Object.keys(object).length > 0; +} + +export function partialPreferencesFromMap( + map: Readonly> +): Partial { + const partial: Partial = {}; + + const auditing: Partial = {}; + const enableAuditLog = parseBoolean(map[PREFERENCE_KEYS.enableAuditLog]); + if (enableAuditLog !== undefined) auditing.enableAuditLog = enableAuditLog; + + const logFieldLevelChanges = parseBoolean(map[PREFERENCE_KEYS.logFieldLevelChanges]); + if (logFieldLevelChanges !== undefined) + auditing.logFieldLevelChanges = logFieldLevelChanges; + + if (hasProperties(auditing)) { + partial.auditing = { + auditing: auditing as GlobalPreferenceValues['auditing']['auditing'], + }; + } + + const formatting: Partial = {}; + const fullDateFormatRaw = map[PREFERENCE_KEYS.fullDateFormat]; + if (fullDateFormatRaw !== undefined) + formatting.fullDateFormat = normalizeFormat(fullDateFormatRaw); + + const monthYearDateFormatRaw = map[PREFERENCE_KEYS.monthYearDateFormat]; + if (monthYearDateFormatRaw !== undefined) { + const monthYearFormat = normalizeFormat(monthYearDateFormatRaw); + if ( + MONTH_YEAR_FORMAT_OPTIONS.includes( + monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number] + ) + ) + formatting.monthYearDateFormat = monthYearFormat; + } + + if (hasProperties(formatting)) { + partial.formatting = { + formatting: formatting as GlobalPreferenceValues['formatting']['formatting'], + }; + } + + const attachments: Partial = {}; + const attachmentThumbnailSize = parseNumber( + map[PREFERENCE_KEYS.attachmentThumbnailSize] ); + if (attachmentThumbnailSize !== undefined) + attachments.attachmentThumbnailSize = attachmentThumbnailSize; - return { - auditing: { - auditing: { - enableAuditLog: parseBoolean( - map[PREFERENCE_KEYS.enableAuditLog], - DEFAULT_VALUES.auditing.auditing.enableAuditLog - ), - logFieldLevelChanges: parseBoolean( - map[PREFERENCE_KEYS.logFieldLevelChanges], - DEFAULT_VALUES.auditing.auditing.logFieldLevelChanges - ), - }, - }, - formatting: { - formatting: { - fullDateFormat, - monthYearDateFormat: MONTH_YEAR_FORMAT_OPTIONS.includes( - monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number] - ) - ? (monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number]) - : DEFAULT_VALUES.formatting.formatting.monthYearDateFormat, - }, - }, - attachments: { - attachments: { - attachmentThumbnailSize: parseNumber( - map[PREFERENCE_KEYS.attachmentThumbnailSize], - DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize - ), - }, - }, - }; -} + if (hasProperties(attachments)) { + partial.attachments = { + attachments: attachments as GlobalPreferenceValues['attachments']['attachments'], + }; + } -export function parseGlobalPreferences( - data: string | null -): { readonly raw: GlobalPreferenceValues; readonly metadata: ReadonlyArray } { - const parsed = parseProperties(data ?? ''); - const values = preferencesFromMap(parsed.map); - return { raw: values, metadata: parsed.lines }; + return partial; } -function normalizeValues( - values: GlobalPreferenceValues | Partial | undefined +export function mergeWithDefaultValues( + values: Partial | undefined, + fallback: GlobalPreferenceValues = globalPreferenceFallback ): GlobalPreferenceValues { - const merged = values ?? DEFAULT_VALUES; + const merged = values ?? {}; return { auditing: { auditing: { enableAuditLog: - merged.auditing?.auditing?.enableAuditLog ?? DEFAULT_VALUES.auditing.auditing.enableAuditLog, + merged.auditing?.auditing?.enableAuditLog ?? + fallback.auditing.auditing.enableAuditLog, logFieldLevelChanges: merged.auditing?.auditing?.logFieldLevelChanges ?? - DEFAULT_VALUES.auditing.auditing.logFieldLevelChanges, + fallback.auditing.auditing.logFieldLevelChanges, }, }, formatting: { formatting: { fullDateFormat: merged.formatting?.formatting?.fullDateFormat ?? - DEFAULT_VALUES.formatting.formatting.fullDateFormat, + fallback.formatting.formatting.fullDateFormat, monthYearDateFormat: merged.formatting?.formatting?.monthYearDateFormat ?? - DEFAULT_VALUES.formatting.formatting.monthYearDateFormat, + fallback.formatting.formatting.monthYearDateFormat, }, }, attachments: { attachments: { attachmentThumbnailSize: merged.attachments?.attachments?.attachmentThumbnailSize ?? - DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize, + fallback.attachments.attachments.attachmentThumbnailSize, }, }, }; } +export function parseGlobalPreferences( + data: string | null +): { + readonly raw: Partial; + readonly metadata: ReadonlyArray; +} { + const parsed = parseProperties(data ?? ''); + const values = partialPreferencesFromMap(parsed.map); + return { raw: values, metadata: parsed.lines }; +} + function preferencesToKeyValue(values: GlobalPreferenceValues): Record { return { [PREFERENCE_KEYS.enableAuditLog]: values.auditing.auditing.enableAuditLog ? 'true' : 'false', @@ -217,9 +253,14 @@ export function applyUpdates( export function serializeGlobalPreferences( raw: GlobalPreferenceValues | Partial | undefined, - metadata: ReadonlyArray + metadata: ReadonlyArray, + options?: { readonly fallback?: GlobalPreferenceValues } ): { readonly data: string; readonly metadata: ReadonlyArray } { - const normalized = normalizeValues(raw as GlobalPreferenceValues | undefined); + const fallback = options?.fallback ?? globalPreferenceFallback; + const normalized = mergeWithDefaultValues( + raw as Partial | undefined, + fallback + ); const { lines, text } = applyUpdates(metadata, preferencesToKeyValue(normalized)); return { data: text, metadata: lines }; } @@ -233,4 +274,4 @@ export function formatGlobalPreferenceValue( if (definition.type === 'java.lang.Integer') return Number(value).toString(); } return String(value ?? ''); -} \ No newline at end of file +} From 4d49db9c76d2a036d2b0b16e2179b741c210856a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 20 Oct 2025 14:31:19 -0400 Subject: [PATCH 088/100] resolve readonly assignment errors in global preferences utils and loader --- .../Preferences/globalPreferencesLoader.ts | 23 +++++++++---- .../Preferences/globalPreferencesUtils.ts | 32 +++++++++---------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index bd55b3485cd..6ec7488872f 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -31,16 +31,20 @@ function mergeMissingFromRemote( const remoteFormatting = remote.formatting?.formatting; if (remoteFormatting !== undefined) { - const currentFormatting = existing.formatting?.formatting ?? {}; - const updatedFormatting: Partial = { - ...currentFormatting, + const currentFormatting = existing.formatting?.formatting; + const updatedFormatting: Record< + keyof GlobalPreferenceValues['formatting']['formatting'], + string | undefined + > = { + fullDateFormat: currentFormatting?.fullDateFormat, + monthYearDateFormat: currentFormatting?.monthYearDateFormat, }; (['fullDateFormat', 'monthYearDateFormat'] as const).forEach((key) => { const remoteValue = remoteFormatting[key]; if ( remoteValue !== undefined && - updatedFormatting[key] === undefined && + (updatedFormatting[key] ?? undefined) === undefined && remoteValue !== DEFAULT_VALUES.formatting.formatting[key] ) { updatedFormatting[key] = remoteValue; @@ -48,13 +52,20 @@ function mergeMissingFromRemote( } }); - if (changed) + if (changed) { + const formattingPayload: Record = {}; + if (updatedFormatting.fullDateFormat !== undefined) + formattingPayload.fullDateFormat = updatedFormatting.fullDateFormat; + if (updatedFormatting.monthYearDateFormat !== undefined) + formattingPayload.monthYearDateFormat = updatedFormatting.monthYearDateFormat; + merged = { ...merged, formatting: { - formatting: updatedFormatting as GlobalPreferenceValues['formatting']['formatting'], + formatting: formattingPayload as GlobalPreferenceValues['formatting']['formatting'], }, }; + } } return { merged, changed }; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 702bc335317..789d9be57f5 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -111,26 +111,26 @@ function hasProperties>(object: T): object is export function partialPreferencesFromMap( map: Readonly> ): Partial { - const partial: Partial = {}; + const partial: Record = {}; - const auditing: Partial = {}; + const auditingValues: Record = {}; const enableAuditLog = parseBoolean(map[PREFERENCE_KEYS.enableAuditLog]); - if (enableAuditLog !== undefined) auditing.enableAuditLog = enableAuditLog; + if (enableAuditLog !== undefined) auditingValues.enableAuditLog = enableAuditLog; const logFieldLevelChanges = parseBoolean(map[PREFERENCE_KEYS.logFieldLevelChanges]); if (logFieldLevelChanges !== undefined) - auditing.logFieldLevelChanges = logFieldLevelChanges; + auditingValues.logFieldLevelChanges = logFieldLevelChanges; - if (hasProperties(auditing)) { + if (hasProperties(auditingValues)) { partial.auditing = { - auditing: auditing as GlobalPreferenceValues['auditing']['auditing'], + auditing: auditingValues, }; } - const formatting: Partial = {}; + const formattingValues: Record = {}; const fullDateFormatRaw = map[PREFERENCE_KEYS.fullDateFormat]; if (fullDateFormatRaw !== undefined) - formatting.fullDateFormat = normalizeFormat(fullDateFormatRaw); + formattingValues.fullDateFormat = normalizeFormat(fullDateFormatRaw); const monthYearDateFormatRaw = map[PREFERENCE_KEYS.monthYearDateFormat]; if (monthYearDateFormatRaw !== undefined) { @@ -140,29 +140,29 @@ export function partialPreferencesFromMap( monthYearFormat as (typeof MONTH_YEAR_FORMAT_OPTIONS)[number] ) ) - formatting.monthYearDateFormat = monthYearFormat; + formattingValues.monthYearDateFormat = monthYearFormat; } - if (hasProperties(formatting)) { + if (hasProperties(formattingValues)) { partial.formatting = { - formatting: formatting as GlobalPreferenceValues['formatting']['formatting'], + formatting: formattingValues, }; } - const attachments: Partial = {}; + const attachmentValues: Record = {}; const attachmentThumbnailSize = parseNumber( map[PREFERENCE_KEYS.attachmentThumbnailSize] ); if (attachmentThumbnailSize !== undefined) - attachments.attachmentThumbnailSize = attachmentThumbnailSize; + attachmentValues.attachmentThumbnailSize = attachmentThumbnailSize; - if (hasProperties(attachments)) { + if (hasProperties(attachmentValues)) { partial.attachments = { - attachments: attachments as GlobalPreferenceValues['attachments']['attachments'], + attachments: attachmentValues, }; } - return partial; + return partial as Partial; } export function mergeWithDefaultValues( From aa5e25198855feb2dd8f6df3ea7529ab4431e445 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 20 Oct 2025 18:35:20 +0000 Subject: [PATCH 089/100] Lint code with ESLint and Prettier Triggered by 4d49db9c76d2a036d2b0b16e2179b741c210856a on branch refs/heads/issue-7442-3 --- .../Preferences/globalPreferencesLoader.ts | 19 ++--- .../Preferences/globalPreferencesUtils.ts | 83 ++++++++++++------- 2 files changed, 63 insertions(+), 39 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index 6ec7488872f..caaf9cf1235 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -1,14 +1,14 @@ import { ajax } from '../../utils/ajax'; import { Http } from '../../utils/ajax/definitions'; import { ping } from '../../utils/ajax/ping'; -import { formatUrl } from '../Router/queryString'; +import { keysToLowerCase } from '../../utils/utils'; import { contextUnlockedPromise, foreverFetch } from '../InitialContext'; import { fetchContext as fetchRemotePrefs, remotePrefs } from '../InitialContext/remotePrefs'; -import { keysToLowerCase } from '../../utils/utils'; +import { formatUrl } from '../Router/queryString'; import { type PartialPreferences } from './BasePreferences'; -import { globalPreferenceDefinitions } from './GlobalDefinitions'; -import { globalPreferences } from './globalPreferences'; +import type { globalPreferenceDefinitions } from './GlobalDefinitions'; import type { GlobalPreferenceValues } from './globalPreferences'; +import { globalPreferences } from './globalPreferences'; import { DEFAULT_VALUES, mergeWithDefaultValues, @@ -78,7 +78,7 @@ export const loadGlobalPreferences = async (): Promise => { return; } - const { data, status, response } = await ajax( + const { data, status, response } = await ajax( formatUrl('/context/app.resource', { name: 'GlobalPreferences', quiet: '', @@ -123,20 +123,17 @@ export const loadGlobalPreferences = async (): Promise => { data: serialized.data, }); - if (typeof resourceId === 'number' && Number.isFinite(resourceId) && resourceId >= 0) - await ping(`${GLOBAL_RESOURCE_URL}${resourceId}/`, { + await (typeof resourceId === 'number' && Number.isFinite(resourceId) && resourceId >= 0 ? ping(`${GLOBAL_RESOURCE_URL}${resourceId}/`, { method: 'PUT', body: payload, headers: { Accept: 'application/json' }, errorMode: 'silent', - }); - else - await ping(GLOBAL_RESOURCE_URL, { + }) : ping(GLOBAL_RESOURCE_URL, { method: 'POST', body: payload, headers: { Accept: 'application/json' }, errorMode: 'silent', - }); + })); } } catch (error) { console.error('Failed migrating remote preferences into GlobalPreferences', error); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 789d9be57f5..232ec98d700 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -1,15 +1,18 @@ -import type { PreferenceItem } from './types'; +import { + FULL_DATE_FORMAT_OPTIONS, + MONTH_YEAR_FORMAT_OPTIONS, +} from './GlobalDefinitions'; import type { GlobalPreferenceValues } from './globalPreferences'; -import { FULL_DATE_FORMAT_OPTIONS, MONTH_YEAR_FORMAT_OPTIONS } from './GlobalDefinitions'; +import type { PreferenceItem } from './types'; export type PropertyLine = - | { readonly type: 'comment' | 'empty'; readonly raw: string } | { readonly type: 'entry'; readonly key: string; readonly value: string; readonly raw: string; - }; + } + | { readonly type: 'comment' | 'empty'; readonly raw: string }; const PREFERENCE_KEYS = { enableAuditLog: 'auditing.do_audits', @@ -20,7 +23,7 @@ const PREFERENCE_KEYS = { } as const; type ParsedProperties = { - readonly lines: ReadonlyArray; + readonly lines: readonly PropertyLine[]; readonly map: Record; }; @@ -68,7 +71,7 @@ function normalizeFormat(value: string): string { function parseProperties(data: string): ParsedProperties { const lines = data.split(/\r?\n/u); - const parsed: PropertyLine[] = []; + const parsed: readonly PropertyLine[] = []; const map: Record = {}; lines.forEach((line) => { @@ -104,7 +107,9 @@ function parseNumber(value: string | undefined): number | undefined { return Number.isNaN(parsed) ? undefined : parsed; } -function hasProperties>(object: T): object is T { +function hasProperties>( + object: T +): object is T { return Object.keys(object).length > 0; } @@ -115,9 +120,12 @@ export function partialPreferencesFromMap( const auditingValues: Record = {}; const enableAuditLog = parseBoolean(map[PREFERENCE_KEYS.enableAuditLog]); - if (enableAuditLog !== undefined) auditingValues.enableAuditLog = enableAuditLog; + if (enableAuditLog !== undefined) + auditingValues.enableAuditLog = enableAuditLog; - const logFieldLevelChanges = parseBoolean(map[PREFERENCE_KEYS.logFieldLevelChanges]); + const logFieldLevelChanges = parseBoolean( + map[PREFERENCE_KEYS.logFieldLevelChanges] + ); if (logFieldLevelChanges !== undefined) auditingValues.logFieldLevelChanges = logFieldLevelChanges; @@ -201,46 +209,57 @@ export function mergeWithDefaultValues( }; } -export function parseGlobalPreferences( - data: string | null -): { +export function parseGlobalPreferences(data: string | null): { readonly raw: Partial; - readonly metadata: ReadonlyArray; + readonly metadata: readonly PropertyLine[]; } { const parsed = parseProperties(data ?? ''); const values = partialPreferencesFromMap(parsed.map); return { raw: values, metadata: parsed.lines }; } -function preferencesToKeyValue(values: GlobalPreferenceValues): Record { +function preferencesToKeyValue( + values: GlobalPreferenceValues +): Record { return { - [PREFERENCE_KEYS.enableAuditLog]: values.auditing.auditing.enableAuditLog ? 'true' : 'false', - [PREFERENCE_KEYS.logFieldLevelChanges]: values.auditing.auditing.logFieldLevelChanges + [PREFERENCE_KEYS.enableAuditLog]: values.auditing.auditing.enableAuditLog ? 'true' : 'false', - [PREFERENCE_KEYS.fullDateFormat]: normalizeFormat(values.formatting.formatting.fullDateFormat), + [PREFERENCE_KEYS.logFieldLevelChanges]: values.auditing.auditing + .logFieldLevelChanges + ? 'true' + : 'false', + [PREFERENCE_KEYS.fullDateFormat]: normalizeFormat( + values.formatting.formatting.fullDateFormat + ), [PREFERENCE_KEYS.monthYearDateFormat]: normalizeFormat( values.formatting.formatting.monthYearDateFormat ), - [PREFERENCE_KEYS.attachmentThumbnailSize]: values.attachments.attachments.attachmentThumbnailSize.toString(), + [PREFERENCE_KEYS.attachmentThumbnailSize]: + values.attachments.attachments.attachmentThumbnailSize.toString(), }; } export function applyUpdates( - lines: ReadonlyArray, + lines: readonly PropertyLine[], updates: Record -): { readonly lines: ReadonlyArray; readonly text: string } { +): { readonly lines: readonly PropertyLine[]; readonly text: string } { const remaining = new Set(Object.keys(updates)); const updatedLines = lines.map((line) => { if (line.type === 'entry' && remaining.has(line.key)) { const value = updates[line.key]; remaining.delete(line.key); - return { type: 'entry', key: line.key, value, raw: `${line.key}=${value}` } as PropertyLine; + return { + type: 'entry', + key: line.key, + value, + raw: `${line.key}=${value}`, + } as PropertyLine; } return line; }); - const appended: PropertyLine[] = Array.from(remaining).map((key) => ({ + const appended: readonly PropertyLine[] = Array.from(remaining, (key) => ({ type: 'entry', key, value: updates[key], @@ -248,20 +267,26 @@ export function applyUpdates( })); const finalLines = [...updatedLines, ...appended]; - return { lines: finalLines, text: finalLines.map((line) => line.raw).join('\n') }; + return { + lines: finalLines, + text: finalLines.map((line) => line.raw).join('\n'), + }; } export function serializeGlobalPreferences( raw: GlobalPreferenceValues | Partial | undefined, - metadata: ReadonlyArray, + metadata: readonly PropertyLine[], options?: { readonly fallback?: GlobalPreferenceValues } -): { readonly data: string; readonly metadata: ReadonlyArray } { +): { readonly data: string; readonly metadata: readonly PropertyLine[] } { const fallback = options?.fallback ?? globalPreferenceFallback; const normalized = mergeWithDefaultValues( raw as Partial | undefined, fallback ); - const { lines, text } = applyUpdates(metadata, preferencesToKeyValue(normalized)); + const { lines, text } = applyUpdates( + metadata, + preferencesToKeyValue(normalized) + ); return { data: text, metadata: lines }; } @@ -270,8 +295,10 @@ export function formatGlobalPreferenceValue( value: unknown ): string { if ('type' in definition) { - if (definition.type === 'java.lang.Boolean') return value ? 'true' : 'false'; - if (definition.type === 'java.lang.Integer') return Number(value).toString(); + if (definition.type === 'java.lang.Boolean') + return value ? 'true' : 'false'; + if (definition.type === 'java.lang.Integer') + return Number(value).toString(); } return String(value ?? ''); } From 86910b6dd022205c0901f749d69290e46bfad4d2 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 30 Oct 2025 21:49:13 -0400 Subject: [PATCH 090/100] ensure Global Preferences read/write the global app resource --- .../Preferences/GlobalDefinitions.ts | 2 +- .../Preferences/globalPreferencesActions.ts | 47 ++++++++++ .../Preferences/globalPreferencesLoader.ts | 45 +++++----- .../Preferences/globalPreferencesResource.ts | 90 +++++++++++++++++++ .../Preferences/globalPreferencesUtils.ts | 20 +++-- .../lib/components/Preferences/index.tsx | 7 +- 6 files changed, 178 insertions(+), 33 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts create mode 100644 specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesResource.ts diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts index 31ee461dbfd..b0a3c0577ff 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/GlobalDefinitions.ts @@ -35,7 +35,7 @@ formatting: { description: preferencesText.fullDateFormatDescription(), requiresReload: false, visible: true, - defaultValue: 'YYYY-MM-DD', + defaultValue: 'yyyy-MM-dd', values: FULL_DATE_FORMAT_OPTIONS.map((value) => ({ value, title: localized(value), diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts new file mode 100644 index 00000000000..5bda36be5e9 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts @@ -0,0 +1,47 @@ +import { remotePrefs } from '../InitialContext/remotePrefs'; +import { + buildGlobalPreferencesPayload, + getGlobalPreferencesMetadata, + setGlobalPreferencesMetadata, + upsertGlobalPreferencesResource, +} from './globalPreferencesResource'; +import { globalPreferences } from './globalPreferences'; +import type { GlobalPreferenceValues } from './globalPreferences'; +import { + getGlobalPreferenceFallback, + globalPreferencesToKeyValue, + mergeWithDefaultValues, + serializeGlobalPreferences, + setGlobalPreferenceFallback, +} from './globalPreferencesUtils'; + +export async function saveGlobalPreferences(): Promise { + const rawValues = globalPreferences.getRaw() as Partial; + const fallback = getGlobalPreferenceFallback(); + const metadata = getGlobalPreferencesMetadata(); + + const { data, metadata: updatedMetadata } = serializeGlobalPreferences( + rawValues as Partial, + metadata, + { fallback } + ); + setGlobalPreferencesMetadata(updatedMetadata); + + const payload = buildGlobalPreferencesPayload(data); + await upsertGlobalPreferencesResource({ + payload, + errorMode: 'dismissible', + }); + + const mergedValues = mergeWithDefaultValues( + rawValues as Partial, + fallback + ); + const keyValues = globalPreferencesToKeyValue(mergedValues); + + Object.entries(keyValues).forEach(([key, value]) => { + remotePrefs[key] = value; + }); + + setGlobalPreferenceFallback(mergedValues); +} diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index caaf9cf1235..a6ddfd75a29 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -1,7 +1,5 @@ import { ajax } from '../../utils/ajax'; import { Http } from '../../utils/ajax/definitions'; -import { ping } from '../../utils/ajax/ping'; -import { keysToLowerCase } from '../../utils/utils'; import { contextUnlockedPromise, foreverFetch } from '../InitialContext'; import { fetchContext as fetchRemotePrefs, remotePrefs } from '../InitialContext/remotePrefs'; import { formatUrl } from '../Router/queryString'; @@ -9,6 +7,12 @@ import { type PartialPreferences } from './BasePreferences'; import type { globalPreferenceDefinitions } from './GlobalDefinitions'; import type { GlobalPreferenceValues } from './globalPreferences'; import { globalPreferences } from './globalPreferences'; +import { + buildGlobalPreferencesPayload, + setGlobalPreferencesMetadata, + setGlobalPreferencesResourceId, + upsertGlobalPreferencesResource, +} from './globalPreferencesResource'; import { DEFAULT_VALUES, mergeWithDefaultValues, @@ -20,8 +24,6 @@ import { type PartialGlobalValues = Partial; -const GLOBAL_RESOURCE_URL = '/context/app.resource/'; - function mergeMissingFromRemote( existing: PartialGlobalValues, remote: PartialGlobalValues @@ -71,6 +73,7 @@ function mergeMissingFromRemote( return { merged, changed }; } +type ResourceResponse = { readonly id: number }; export const loadGlobalPreferences = async (): Promise => { const entryPoint = await contextUnlockedPromise; if (entryPoint !== 'main') { @@ -91,6 +94,12 @@ export const loadGlobalPreferences = async (): Promise => { ); await fetchRemotePrefs.catch(() => undefined); + const resourceIdHeader = response.headers.get('X-Record-ID'); + const parsedResourceId = + resourceIdHeader === null ? undefined : Number.parseInt(resourceIdHeader, 10); + setGlobalPreferencesResourceId( + Number.isFinite(parsedResourceId) ? parsedResourceId : undefined + ); const remotePartial = partialPreferencesFromMap(remotePrefs); const remoteDefaults = mergeWithDefaultValues(remotePartial); @@ -102,6 +111,7 @@ export const loadGlobalPreferences = async (): Promise => { const { raw, metadata } = parseGlobalPreferences( status === Http.NO_CONTENT ? null : data ); + setGlobalPreferencesMetadata(metadata); const { merged: migratedRaw, changed } = mergeMissingFromRemote(raw, remotePartial); @@ -110,34 +120,21 @@ export const loadGlobalPreferences = async (): Promise => { const serialized = serializeGlobalPreferences(migratedRaw, metadata, { fallback: remoteDefaults, }); + setGlobalPreferencesMetadata(serialized.metadata); const originalData = status === Http.NO_CONTENT ? '' : data; if (serialized.data.trim() !== originalData.trim()) { - const resourceIdHeader = response.headers.get('X-Record-ID'); - const resourceId = - resourceIdHeader === null ? undefined : Number.parseInt(resourceIdHeader, 10); - const payload = keysToLowerCase({ - name: 'GlobalPreferences', - mimeType: 'text/plain', - metaData: '', - data: serialized.data, + const payload = buildGlobalPreferencesPayload(serialized.data); + await upsertGlobalPreferencesResource({ + payload, + errorMode: 'silent', }); - - await (typeof resourceId === 'number' && Number.isFinite(resourceId) && resourceId >= 0 ? ping(`${GLOBAL_RESOURCE_URL}${resourceId}/`, { - method: 'PUT', - body: payload, - headers: { Accept: 'application/json' }, - errorMode: 'silent', - }) : ping(GLOBAL_RESOURCE_URL, { - method: 'POST', - body: payload, - headers: { Accept: 'application/json' }, - errorMode: 'silent', - })); } } catch (error) { console.error('Failed migrating remote preferences into GlobalPreferences', error); } + } else { + setGlobalPreferencesMetadata(metadata); } globalPreferences.setRaw( diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesResource.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesResource.ts new file mode 100644 index 00000000000..26e53feae08 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesResource.ts @@ -0,0 +1,90 @@ +import { ajax } from '../../utils/ajax'; +import type { AjaxErrorMode } from '../../utils/ajax'; +import { Http } from '../../utils/ajax/definitions'; +import type { IR } from '../../utils/types'; +import { keysToLowerCase } from '../../utils/utils'; +import type { PropertyLine } from './globalPreferencesUtils'; + +type ResourceResponse = { readonly id: number }; + +export const GLOBAL_RESOURCE_URL = '/context/collection_resource/'; + +let globalPreferencesMetadata: ReadonlyArray = []; +let globalPreferencesResourceId: number | undefined; + +export const getGlobalPreferencesMetadata = (): ReadonlyArray => + globalPreferencesMetadata; + +export const setGlobalPreferencesMetadata = ( + metadata: ReadonlyArray +): void => { + globalPreferencesMetadata = metadata; +}; + +export const getGlobalPreferencesResourceId = (): number | undefined => + globalPreferencesResourceId; + +export const setGlobalPreferencesResourceId = ( + id: number | undefined +): void => { + globalPreferencesResourceId = id; +}; + +export const buildGlobalPreferencesPayload = ( + data: string +): IR => + keysToLowerCase({ + name: 'GlobalPreferences', + mimeType: 'text/plain', + metaData: '', + data, + }); + +export const upsertGlobalPreferencesResource = async ({ + payload, + errorMode, +}: { + readonly payload: IR; + readonly errorMode: AjaxErrorMode; +}): Promise => { + const resourceId = getGlobalPreferencesResourceId(); + let shouldCreate = true; + + if ( + typeof resourceId === 'number' && + Number.isFinite(resourceId) && + resourceId >= 0 + ) { + const { status } = await ajax( + `${GLOBAL_RESOURCE_URL}${resourceId}/`, + { + method: 'PUT', + body: payload, + headers: { Accept: 'text/plain' }, + errorMode, + expectedErrors: [Http.NOT_FOUND], + } + ); + shouldCreate = status === Http.NOT_FOUND; + } + + if (shouldCreate) { + const { data, status } = await ajax( + GLOBAL_RESOURCE_URL, + { + method: 'POST', + body: payload, + headers: { Accept: 'application/json' }, + errorMode, + expectedErrors: [Http.CREATED], + } + ); + setGlobalPreferencesResourceId( + status === Http.CREATED && typeof data?.id === 'number' + ? data.id + : undefined + ); + } else { + setGlobalPreferencesResourceId(resourceId); + } +}; diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 232ec98d700..4ddf3ce58eb 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -27,10 +27,12 @@ type ParsedProperties = { readonly map: Record; }; -const DATE_FORMAT_NORMALIZER = new Set([ - ...FULL_DATE_FORMAT_OPTIONS, - ...MONTH_YEAR_FORMAT_OPTIONS, -]); +const DATE_FORMAT_OPTIONS_LOOKUP = new Map( + [...FULL_DATE_FORMAT_OPTIONS, ...MONTH_YEAR_FORMAT_OPTIONS].map((value) => [ + value.toUpperCase(), + value, + ]) +); export const DEFAULT_VALUES: GlobalPreferenceValues = { auditing: { @@ -41,7 +43,7 @@ export const DEFAULT_VALUES: GlobalPreferenceValues = { }, formatting: { formatting: { - fullDateFormat: 'YYYY-MM-DD', + fullDateFormat: 'yyyy-MM-dd', monthYearDateFormat: 'YYYY-MM', }, }, @@ -66,7 +68,7 @@ export function getGlobalPreferenceFallback(): GlobalPreferenceValues { function normalizeFormat(value: string): string { const upper = value.toUpperCase(); - return DATE_FORMAT_NORMALIZER.has(upper) ? upper : upper; + return DATE_FORMAT_OPTIONS_LOOKUP.get(upper) ?? value; } function parseProperties(data: string): ParsedProperties { @@ -302,3 +304,9 @@ export function formatGlobalPreferenceValue( } return String(value ?? ''); } + +export function globalPreferencesToKeyValue( + values: GlobalPreferenceValues +): Record { + return preferencesToKeyValue(values); +} diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 7ba5ddb6693..a9bcd61708e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -31,6 +31,7 @@ import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { collectionPreferences } from './collectionPreferences'; import { globalPreferences } from './globalPreferences'; +import { saveGlobalPreferences } from './globalPreferencesActions'; import { loadGlobalPreferences } from './globalPreferencesLoader'; import { useDarkMode } from './Hooks'; import { DefaultPreferenceItemRender } from './Renderers'; @@ -152,8 +153,10 @@ function Preferences({ className="contents" onSubmit={(): void => loading( - basePreferences - .awaitSynced() + (prefType === 'global' + ? saveGlobalPreferences() + : basePreferences.awaitSynced() + ) .then(() => needsRestart ? globalThis.location.assign('/specify/') From a70172a2b241ceb7d584b435944543eaf0cbf763 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 30 Oct 2025 21:58:51 -0400 Subject: [PATCH 091/100] satisfy TS readonly types in global preferences flow --- .../lib/components/Preferences/globalPreferencesActions.ts | 3 ++- .../lib/components/Preferences/globalPreferencesLoader.ts | 1 - .../lib/components/Preferences/globalPreferencesUtils.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts index 5bda36be5e9..eb95be77eb2 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesActions.ts @@ -39,8 +39,9 @@ export async function saveGlobalPreferences(): Promise { ); const keyValues = globalPreferencesToKeyValue(mergedValues); + const mutableRemotePrefs = remotePrefs as Record; Object.entries(keyValues).forEach(([key, value]) => { - remotePrefs[key] = value; + mutableRemotePrefs[key] = value; }); setGlobalPreferenceFallback(mergedValues); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index a6ddfd75a29..be8e8e5c63e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -73,7 +73,6 @@ function mergeMissingFromRemote( return { merged, changed }; } -type ResourceResponse = { readonly id: number }; export const loadGlobalPreferences = async (): Promise => { const entryPoint = await contextUnlockedPromise; if (entryPoint !== 'main') { diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts index 4ddf3ce58eb..4f7a3f184b6 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesUtils.ts @@ -73,7 +73,7 @@ function normalizeFormat(value: string): string { function parseProperties(data: string): ParsedProperties { const lines = data.split(/\r?\n/u); - const parsed: readonly PropertyLine[] = []; + const parsed: PropertyLine[] = []; const map: Record = {}; lines.forEach((line) => { From d710796345ceb9c6ffeedccdd88e3b0bb318e742 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Thu, 30 Oct 2025 22:09:45 -0400 Subject: [PATCH 092/100] align tests with normalized formats and mock headers --- .../Preferences/__tests__/globalPreferencesUtils.test.ts | 6 +++--- .../lib/components/Preferences/globalPreferencesLoader.ts | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts index 8bc37ed215f..123f046e694 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts @@ -17,7 +17,7 @@ describe('globalPreferencesUtils', () => { 'auditing.do_audits': 'false', }); - expect(partial.formatting?.formatting?.fullDateFormat).toBe('MM/DD/YYYY'); + expect(partial.formatting?.formatting?.fullDateFormat).toBe('MM/dd/yyyy'); expect(partial.auditing?.auditing?.enableAuditLog).toBe(false); expect(partial.attachments).toBeUndefined(); }); @@ -29,7 +29,7 @@ describe('globalPreferencesUtils', () => { const merged = mergeWithDefaultValues(remotePartial); - expect(merged.formatting.formatting.fullDateFormat).toBe('MM/DD/YYYY'); + expect(merged.formatting.formatting.fullDateFormat).toBe('MM/dd/yyyy'); expect(merged.attachments.attachments.attachmentThumbnailSize).toBe( DEFAULT_VALUES.attachments.attachments.attachmentThumbnailSize ); @@ -45,7 +45,7 @@ describe('globalPreferencesUtils', () => { const { data } = serializeGlobalPreferences(undefined, [], { fallback }); - expect(data).toContain('ui.formatting.scrdateformat=MM/DD/YYYY'); + expect(data).toContain('ui.formatting.scrdateformat=MM/dd/yyyy'); expect(data).toContain('auditing.do_audits=true'); }); }); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index be8e8e5c63e..df87b873854 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -93,7 +93,10 @@ export const loadGlobalPreferences = async (): Promise => { ); await fetchRemotePrefs.catch(() => undefined); - const resourceIdHeader = response.headers.get('X-Record-ID'); + const resourceIdHeader = + typeof response === 'object' && 'headers' in response + ? response.headers.get('X-Record-ID') + : null; const parsedResourceId = resourceIdHeader === null ? undefined : Number.parseInt(resourceIdHeader, 10); setGlobalPreferencesResourceId( From fa32925a9ae2807f6605da5d235d46fb0be16788 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Fri, 31 Oct 2025 02:14:14 +0000 Subject: [PATCH 093/100] Lint code with ESLint and Prettier Triggered by d710796345ceb9c6ffeedccdd88e3b0bb318e742 on branch refs/heads/issue-7442-3 --- .../__tests__/globalPreferencesUtils.test.ts | 6 ++-- .../Preferences/globalPreferencesLoader.ts | 29 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts index 123f046e694..b1feaac28b3 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/__tests__/globalPreferencesUtils.test.ts @@ -11,7 +11,7 @@ describe('globalPreferencesUtils', () => { setGlobalPreferenceFallback(DEFAULT_VALUES); }); - it('builds partial values from remote preferences map', () => { + test('builds partial values from remote preferences map', () => { const partial = partialPreferencesFromMap({ 'ui.formatting.scrdateformat': 'MM/dd/yyyy', 'auditing.do_audits': 'false', @@ -22,7 +22,7 @@ describe('globalPreferencesUtils', () => { expect(partial.attachments).toBeUndefined(); }); - it('merges partial values with fallback defaults', () => { + test('merges partial values with fallback defaults', () => { const remotePartial = partialPreferencesFromMap({ 'ui.formatting.scrdateformat': 'MM/dd/yyyy', }); @@ -35,7 +35,7 @@ describe('globalPreferencesUtils', () => { ); }); - it('serializes using configured fallback values', () => { + test('serializes using configured fallback values', () => { const fallback = mergeWithDefaultValues( partialPreferencesFromMap({ 'ui.formatting.scrdateformat': 'MM/dd/yyyy', diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts index df87b873854..076d3f288e6 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts +++ b/specifyweb/frontend/js_src/lib/components/Preferences/globalPreferencesLoader.ts @@ -1,7 +1,10 @@ import { ajax } from '../../utils/ajax'; import { Http } from '../../utils/ajax/definitions'; import { contextUnlockedPromise, foreverFetch } from '../InitialContext'; -import { fetchContext as fetchRemotePrefs, remotePrefs } from '../InitialContext/remotePrefs'; +import { + fetchContext as fetchRemotePrefs, + remotePrefs, +} from '../InitialContext/remotePrefs'; import { formatUrl } from '../Router/queryString'; import { type PartialPreferences } from './BasePreferences'; import type { globalPreferenceDefinitions } from './GlobalDefinitions'; @@ -59,12 +62,14 @@ function mergeMissingFromRemote( if (updatedFormatting.fullDateFormat !== undefined) formattingPayload.fullDateFormat = updatedFormatting.fullDateFormat; if (updatedFormatting.monthYearDateFormat !== undefined) - formattingPayload.monthYearDateFormat = updatedFormatting.monthYearDateFormat; + formattingPayload.monthYearDateFormat = + updatedFormatting.monthYearDateFormat; merged = { ...merged, formatting: { - formatting: formattingPayload as GlobalPreferenceValues['formatting']['formatting'], + formatting: + formattingPayload as GlobalPreferenceValues['formatting']['formatting'], }, }; } @@ -98,7 +103,9 @@ export const loadGlobalPreferences = async (): Promise => { ? response.headers.get('X-Record-ID') : null; const parsedResourceId = - resourceIdHeader === null ? undefined : Number.parseInt(resourceIdHeader, 10); + resourceIdHeader === null + ? undefined + : Number.parseInt(resourceIdHeader, 10); setGlobalPreferencesResourceId( Number.isFinite(parsedResourceId) ? parsedResourceId : undefined ); @@ -115,7 +122,10 @@ export const loadGlobalPreferences = async (): Promise => { ); setGlobalPreferencesMetadata(metadata); - const { merged: migratedRaw, changed } = mergeMissingFromRemote(raw, remotePartial); + const { merged: migratedRaw, changed } = mergeMissingFromRemote( + raw, + remotePartial + ); if (changed) { try { @@ -133,13 +143,18 @@ export const loadGlobalPreferences = async (): Promise => { }); } } catch (error) { - console.error('Failed migrating remote preferences into GlobalPreferences', error); + console.error( + 'Failed migrating remote preferences into GlobalPreferences', + error + ); } } else { setGlobalPreferencesMetadata(metadata); } globalPreferences.setRaw( - (changed ? migratedRaw : raw) as PartialPreferences + (changed ? migratedRaw : raw) as PartialPreferences< + typeof globalPreferenceDefinitions + > ); }; From 4598929c6b761b427e208578118b33051f854d8a Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 2 Nov 2025 18:49:13 -0500 Subject: [PATCH 094/100] Fix remote pref definitions regression --- .../components/InitialContext/remotePrefs.ts | 22 +++---------------- .../lib/components/Preferences/Editor.tsx | 1 - 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts index 5e48ef61c29..d069c3281b2 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/remotePrefs.ts @@ -159,6 +159,9 @@ export const remotePrefsDefinitions = f.store( 'auditing.audit_field_updates': { description: 'Whether Audit Log records field value changes', defaultValue: true, + parser: 'java.lang.Boolean', + isLegacy: true, + }, 'sp7.allow_adding_child_to_synonymized_parent.GeologicTimePeriod': { description: 'Allowed to add children to synopsized Geologic Time Period records', @@ -211,25 +214,6 @@ export const remotePrefsDefinitions = f.store( parser: 'java.lang.Boolean', isLegacy: false, }, - 'attachment.preview_size': { - description: 'The size in px of the generated attachment thumbnails', - defaultValue: 123, - parser: 'java.lang.Long', - isLegacy: true, - }, - // These are used on the back end only: - 'auditing.do_audits': { - description: 'Whether Audit Log is enabled', - defaultValue: true, - parser: 'java.lang.Boolean', - isLegacy: true, - }, - 'auditing.audit_field_updates': { - description: 'Whether Audit Log records field value changes', - defaultValue: true, - parser: 'java.lang.Boolean', - isLegacy: true, - }, // This is actually stored in Global Prefs: /* * 'AUDIT_LIFESPAN_MONTHS': { diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx index 01af629e69e..475357d5770 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Editor.tsx @@ -234,4 +234,3 @@ export const GlobalPreferencesEditor = createPreferencesEditor({ parse: parseGlobalPreferenceData, serialize: serializeGlobalPreferenceData, }); -}); From 21fdc3f04ceb3300637d231ee75605155c1dfe46 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 2 Nov 2025 18:55:46 -0500 Subject: [PATCH 095/100] Fix collection pref usage and clean duplicate localization keys --- .../js_src/lib/components/FormFields/Field.tsx | 4 ++-- .../js_src/lib/localization/attachments.ts | 7 ------- .../js_src/lib/localization/specifyNetwork.ts | 14 -------------- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index 1fb8295b8dc..e3c5121d15e 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -139,13 +139,13 @@ function Field({ // Check if collection pref wants to inherit primary cat num for empty CO cat num sibilings inside of a COG const [displayPrimaryCatNumberPref] = collectionPreferences.use( 'catalogNumberInheritance', - 'collectionObject', + 'behavior', 'inheritance' ); // Check if collection pref wants to inherit parent cat num for empty CO cat num children const [displayParentCatNumberPref] = collectionPreferences.use( - 'catalogNumberInheritance', + 'catalogNumberParentInheritance', 'component', 'inheritance' ); diff --git a/specifyweb/frontend/js_src/lib/localization/attachments.ts b/specifyweb/frontend/js_src/lib/localization/attachments.ts index d4d80513885..c01262d08ef 100644 --- a/specifyweb/frontend/js_src/lib/localization/attachments.ts +++ b/specifyweb/frontend/js_src/lib/localization/attachments.ts @@ -696,11 +696,4 @@ export const attachmentsText = createDictionary({ 'en-us': 'This controls whether or not new attachments added to this collection are flagged as "Public" by default. Public attachments will automatically be visible on a Specify Web Portal. This setting can be overridden on a per-attachment basis and does not affect existing attachments.', }, - publicDefault: { - 'en-us': 'Make Attachments Public by Default', - }, - publicDefaultDescription: { - 'en-us': - 'This controls whether or not new attachments added to this collection are flagged as "Public" by default. Public attachments will automatically be visible on a Specify Web Portal. This setting can be overridden on a per-attachment basis and does not affect existing attachments.', - }, } as const); diff --git a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts index b2e05a8bd1d..08d37debb90 100644 --- a/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts +++ b/specifyweb/frontend/js_src/lib/localization/specifyNetwork.ts @@ -30,20 +30,6 @@ export const specifyNetworkText = createDictionary({ 'en-us': 'The GBIF "dataSetKey" (a UUID) for this collection, used for Specify Network integration.', }, - publishingOrganizationKey: { - 'en-us': 'GBIF Publishing Organization Key', - }, - publishingOrganizationKeyDescription: { - 'en-us': - 'The GBIF "publishingOrgKey" (a UUID) for this collection, used for Specify Network integration.', - }, - collectionKey: { - 'en-us': 'GBIF Data Set Key', - }, - collectionKeyDescription: { - 'en-us': - 'The GBIF "dataSetKey" (a UUID) for this collection, used for Specify Network integration.', - }, occurrenceOrGuidRequired: { 'en-us': 'Species Name or GUID must be provided to display this page', 'de-ch': From 1a9946b11629c3da6d96e8af85e1491a9ab715ee Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 2 Nov 2025 19:36:01 -0500 Subject: [PATCH 096/100] resolved merge conflicts --- .../lib/components/FormFields/Field.tsx | 4 +- .../Preferences/CollectionDefinitions.tsx | 41 +------------------ 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx index e3c5121d15e..023306db8c6 100644 --- a/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx +++ b/specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx @@ -146,7 +146,7 @@ function Field({ // Check if collection pref wants to inherit parent cat num for empty CO cat num children const [displayParentCatNumberPref] = collectionPreferences.use( 'catalogNumberParentInheritance', - 'component', + 'behavior', 'inheritance' ); @@ -266,4 +266,4 @@ export function useRightAlignClassName( globalThis.navigator.userAgent.toLowerCase().includes('webkit') ? `text-right ${isReadOnly ? '' : 'pr-6'}` : ''; -} +} \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 80b10f7273a..e97c0a8fde0 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -228,20 +228,8 @@ export const collectionPreferenceDefinitions = { defaultValue: false, type: 'java.lang.Boolean', }), - refreshRate: definePref({ - title: statsText.autoRefreshRate(), - description: statsText.autoRefreshRateDescription(), - requiresReload: false, - visible: true, - defaultValue: 24, - type: 'java.lang.Integer', - }), }, }, - specifyNetwork: { - title: specifyNetworkText.specifyNetwork(), - items: specifyNetworkItems, - }, }, }, catalogNumberParentInheritance: { @@ -274,33 +262,6 @@ export const collectionPreferenceDefinitions = { }), }, }, - component: { - title: () => camelToHuman('Component'), - items: { - inheritance: definePref({ - title: () => - preferencesText.inheritanceCatNumberParentCOPref({ - catalogNumber: getField( - tables.CollectionObject, - 'catalogNumber' - ).label, - collectionObject: tables.CollectionObject.label, - }), - description: () => - preferencesText.inheritanceCatNumberParentCOPrefDescription({ - catalogNumber: getField( - tables.CollectionObject, - 'catalogNumber' - ).label, - collectionObject: tables.CollectionObject.label, - }), - requiresReload: false, - visible: true, - defaultValue: false, - type: 'java.lang.Boolean', - }), - }, - }, }, }, @@ -325,4 +286,4 @@ export const collectionPreferenceDefinitions = { }, } as const; -ensure()(collectionPreferenceDefinitions); +ensure()(collectionPreferenceDefinitions); \ No newline at end of file From 03b82ffde56fa5ab89336d7b83b8acfbd04c4383 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Mon, 3 Nov 2025 00:40:04 +0000 Subject: [PATCH 097/100] Lint code with ESLint and Prettier Triggered by 1a9946b11629c3da6d96e8af85e1491a9ab715ee on branch refs/heads/issue-7442-3 --- .../js_src/lib/components/Preferences/CollectionDefinitions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index e97c0a8fde0..a8944be59fc 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -286,4 +286,4 @@ export const collectionPreferenceDefinitions = { }, } as const; -ensure()(collectionPreferenceDefinitions); \ No newline at end of file +ensure()(collectionPreferenceDefinitions); From 969eceb2be49739411d31d693ccec219b6a61961 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 16 Nov 2025 15:58:20 +0000 Subject: [PATCH 098/100] Lint code with ESLint and Prettier Triggered by 860f25b7bf5b60a9b820b10b7f43c06c2f8212fe on branch refs/heads/issue-7442-3 --- .../__tests__/AppResourcesAside.test.tsx | 20 +++++++++---------- .../components/AppResources/filtersHelpers.ts | 5 ++++- .../js_src/lib/components/Atoms/Icons.tsx | 2 +- .../lib/components/Preferences/Aside.tsx | 9 ++------- .../lib/components/Preferences/index.tsx | 13 ++++++------ 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx index 422557d21ab..bb4bf7ab42a 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/AppResourcesAside.test.tsx @@ -140,16 +140,16 @@ describe('AppResourcesAside (expanded case)', () => { unmount: unmountExpandedll, container: expandedContainer, } = mount( - - - - ); + + + + ); const expandedAllFragment = asFragmentAllExpanded().textContent; diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts b/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts index 674e503ccb8..0472434db54 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/filtersHelpers.ts @@ -78,7 +78,10 @@ export const getAppResourceType = ( if (matchedType !== undefined) return matchedType; - if (normalize(resource.name) === 'preferences' && normalize(resource.mimeType) === undefined) + if ( + normalize(resource.name) === 'preferences' && + normalize(resource.mimeType) === undefined + ) return 'otherPropertiesResource'; return 'otherAppResources'; diff --git a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx index 972395225bb..9ee3b99a0a2 100644 --- a/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx +++ b/specifyweb/frontend/js_src/lib/components/Atoms/Icons.tsx @@ -107,7 +107,7 @@ export const icons = { minus: , minusCircle: , nonStrict: , - office: , + office: , pencil: , pencilAt: , photos: , diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx index 59b75fbf019..43bb89fdcce 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx @@ -47,10 +47,7 @@ export function PreferencesAside({ const visibleDefinitions = React.useMemo( () => definitions - .map( - (definition, index) => - [index, definition] as const - ) + .map((definition, index) => [index, definition] as const) .filter( ([, [category]]) => !( @@ -82,9 +79,7 @@ export function PreferencesAside({ > {visibleDefinitions.map(([definitionIndex, [category, { title }]]) => ( setFreezeCategory(definitionIndex)} diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 6535f307b7c..14a8b75a5b9 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -10,8 +10,8 @@ import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { headerText } from '../../localization/header'; -import { resourcesText } from '../../localization/resources'; import { preferencesText } from '../../localization/preferences'; +import { resourcesText } from '../../localization/resources'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; import type { IR } from '../../utils/types'; @@ -31,8 +31,8 @@ import { import { PreferencesAside } from './Aside'; import type { BasePreferences } from './BasePreferences'; import { collectionPreferenceDefinitions } from './CollectionDefinitions'; -import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { collectionPreferences } from './collectionPreferences'; +import { globalPreferenceDefinitions } from './GlobalDefinitions'; import { globalPreferences } from './globalPreferences'; import { saveGlobalPreferences } from './globalPreferencesActions'; import { loadGlobalPreferences } from './globalPreferencesLoader'; @@ -159,12 +159,11 @@ function Preferences({ (prefType === 'global' ? saveGlobalPreferences() : basePreferences.awaitSynced() + ).then(() => + needsRestart + ? globalThis.location.assign('/specify/') + : navigate('/specify/') ) - .then(() => - needsRestart - ? globalThis.location.assign('/specify/') - : navigate('/specify/') - ) ) } > From 88a5a2197293dc3be04947f5c9c7aa81e10c54e8 Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 16 Nov 2025 18:46:53 -0500 Subject: [PATCH 099/100] Restrict global pref accessibilty to authorised roles --- .../__tests__/useResourcesTree.test.ts | 23 ++++++++++ .../lib/components/AppResources/tree.ts | 42 ++++++++++++------- .../lib/components/Preferences/index.tsx | 8 ++-- 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index 86493b68328..3922b3f9e37 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -7,6 +7,7 @@ import type { AppResourcesTree } from '../hooks'; import { useResourcesTree } from '../hooks'; import { staticAppResources } from './staticAppResources'; import { utilsForTests } from './utils'; +import { userInformation } from '../../InitialContext/userInformation'; requireContext(); @@ -79,4 +80,26 @@ describe('useResourcesTree', () => { expect(getResourceCountTree(result.current)).toBe(4); }); + + test('hides global preferences for non-admin users', () => { + const originalIsAdmin = userInformation.isadmin; + Object.defineProperty(userInformation, 'isadmin', { + value: false, + configurable: true, + writable: true, + }); + + const { result } = renderHook(() => useResourcesTree(resources)); + + const flattened = flattenResources(result.current); + expect(flattened.map(({ label, name }) => label ?? name)).not.toContain( + 'Global Preferences' + ); + + Object.defineProperty(userInformation, 'isadmin', { + value: originalIsAdmin, + configurable: true, + writable: true, + }); + }); }); diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts index e971d58a6aa..330057c76ad 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts @@ -7,6 +7,7 @@ import { sortFunction } from '../../utils/utils'; import { addMissingFields } from '../DataModel/addMissingFields'; import type { SerializedResource } from '../DataModel/helperTypes'; import { getResourceApiUrl } from '../DataModel/resource'; +import { userInformation } from '../InitialContext/userInformation'; import type { Collection, Discipline, @@ -109,21 +110,32 @@ const disambiguateGlobalPrefs = ( appResources: RA>, directories: RA> ): AppResourcesTree[number]['appResources'] => - appResources.map((resource) => { - if (resource.name !== prefResource) return resource; - const directory = directories.find( - ({ id }) => - getResourceApiUrl('SpAppResourceDir', id) === resource.spAppResourceDir - ); - // Pretty sure this is redundant... that is, directory should always be defined. - if (!directory) return resource; - const userType = directory.userType?.toLowerCase(); - if (userType === globalUserType) - return { ...resource, label: resourcesText.globalPreferences() }; - else if (userType === remoteUserType) - return { ...resource, label: resourcesText.remotePreferences() }; - else return resource; - }); + appResources + .filter((resource) => { + if (resource.name !== prefResource) return true; + const directory = directories.find( + ({ id }) => + getResourceApiUrl('SpAppResourceDir', id) === resource.spAppResourceDir + ); + const userType = directory?.userType?.toLowerCase(); + const isGlobalPrefs = userType === globalUserType; + return !(isGlobalPrefs && userInformation.isadmin === false); + }) + .map((resource) => { + if (resource.name !== prefResource) return resource; + const directory = directories.find( + ({ id }) => + getResourceApiUrl('SpAppResourceDir', id) === resource.spAppResourceDir + ); + // Pretty sure this is redundant... that is, directory should always be defined. + if (!directory) return resource; + const userType = directory.userType?.toLowerCase(); + if (userType === globalUserType) + return { ...resource, label: resourcesText.globalPreferences() }; + else if (userType === remoteUserType) + return { ...resource, label: resourcesText.remotePreferences() }; + else return resource; + }); /** * Merge resources from several directories into a single one. diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 14a8b75a5b9..eaea3bc86b4 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -600,8 +600,10 @@ export function GlobalPreferencesWrapper(): JSX.Element | null { function GlobalPreferences(): JSX.Element { return ( - - - + + + + + ); } From 6e0489b9837a731bca1d8751d11482f68060a92c Mon Sep 17 00:00:00 2001 From: Gitesh Sagvekar Date: Sun, 16 Nov 2025 23:50:55 +0000 Subject: [PATCH 100/100] Lint code with ESLint and Prettier Triggered by 88a5a2197293dc3be04947f5c9c7aa81e10c54e8 on branch refs/heads/issue-7442-3 --- .../AppResources/__tests__/useResourcesTree.test.ts | 6 +++--- .../js_src/lib/components/AppResources/tree.ts | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts index 3922b3f9e37..2cf3330c9b8 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/__tests__/useResourcesTree.test.ts @@ -2,12 +2,12 @@ import { renderHook } from '@testing-library/react'; import type { LocalizedString } from 'typesafe-i18n'; import { requireContext } from '../../../tests/helpers'; +import { userInformation } from '../../InitialContext/userInformation'; import { getAppResourceCount } from '../helpers'; import type { AppResourcesTree } from '../hooks'; import { useResourcesTree } from '../hooks'; import { staticAppResources } from './staticAppResources'; import { utilsForTests } from './utils'; -import { userInformation } from '../../InitialContext/userInformation'; requireContext(); @@ -15,10 +15,10 @@ const { setAppResourceDir, testDisciplines } = utilsForTests; const flattenResources = ( tree: AppResourcesTree -): ReadonlyArray<{ +): readonly { readonly name: string | undefined; readonly label: LocalizedString | undefined; -}> => +}[] => tree.flatMap(({ appResources, subCategories }) => [ ...appResources.map((resource) => ({ name: resource.name, diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts index 330057c76ad..2821db8b4b2 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts +++ b/specifyweb/frontend/js_src/lib/components/AppResources/tree.ts @@ -1,4 +1,3 @@ - import { resourcesText } from '../../localization/resources'; import { userText } from '../../localization/user'; import type { RA } from '../../utils/types'; @@ -7,7 +6,6 @@ import { sortFunction } from '../../utils/utils'; import { addMissingFields } from '../DataModel/addMissingFields'; import type { SerializedResource } from '../DataModel/helperTypes'; import { getResourceApiUrl } from '../DataModel/resource'; -import { userInformation } from '../InitialContext/userInformation'; import type { Collection, Discipline, @@ -15,6 +13,7 @@ import type { SpAppResourceDir, SpViewSetObj, } from '../DataModel/types'; +import { userInformation } from '../InitialContext/userInformation'; import { userTypes } from '../PickLists/definitions'; import type { AppResources, AppResourcesTree } from './hooks'; import type { AppResourceScope, ScopedAppResourceDir } from './types'; @@ -115,17 +114,19 @@ const disambiguateGlobalPrefs = ( if (resource.name !== prefResource) return true; const directory = directories.find( ({ id }) => - getResourceApiUrl('SpAppResourceDir', id) === resource.spAppResourceDir + getResourceApiUrl('SpAppResourceDir', id) === + resource.spAppResourceDir ); const userType = directory?.userType?.toLowerCase(); const isGlobalPrefs = userType === globalUserType; - return !(isGlobalPrefs && userInformation.isadmin === false); + return !(isGlobalPrefs && !userInformation.isadmin); }) .map((resource) => { if (resource.name !== prefResource) return resource; const directory = directories.find( ({ id }) => - getResourceApiUrl('SpAppResourceDir', id) === resource.spAppResourceDir + getResourceApiUrl('SpAppResourceDir', id) === + resource.spAppResourceDir ); // Pretty sure this is redundant... that is, directory should always be defined. if (!directory) return resource;