Skip to content

Commit ccda46d

Browse files
calixtuskoppor
andauthored
Consistent casing in fieldnames (#13867)
* Prototype * Add ADR * Modify ADR * Fix tests * Fix merge conflict * CHANGELOG.md * Fix APA fields * Refine ADR * typo * Code formatting * Remove capitalization from orfield * Comment to ADR * Remove forced lowercase of field names in bib file * Fix tests * Fix more tests * Add special case for OpenOffice/LibreOffice * Rewrite * Suggestions * Fix tests --------- Co-authored-by: Oliver Kopp <[email protected]> Co-authored-by: Carl Christian Snethlage <[email protected]>
1 parent befa771 commit ccda46d

File tree

59 files changed

+606
-489
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+606
-489
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
126126
- We fixed an issue with double display of the library filename in the tab tooltip in the case of a changed library. [#13781](https://github.com/JabRef/jabref/pull/13781)
127127
- When creating a library, if you drag a PDF file containing only a single column, the dialog will now automatically close. [#13262](https://github.com/JabRef/jabref/issues/13262)
128128
- We fixed an issue where the tab showing the fulltext search results would appear blank after switching libraries. [#13241](https://github.com/JabRef/jabref/issues/13241)
129+
- We fixed an issue where field names were inconsistently capitalized. [#10590](https://github.com/JabRef/jabref/issues/10590)
129130
- We fixed an issue where "Copy to" was enabled even if no other library was opened. [#13280](https://github.com/JabRef/jabref/pull/13280)
130131
- We fixed an issue where the groups were still displayed after closing all libraries. [#13382](https://github.com/JabRef/jabref/issues/13382)
131132
- Enhanced field selection logic in the Merge Entries dialog when fetching from DOI to prefer valid years and entry types. [#12549](https://github.com/JabRef/jabref/issues/12549)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
---
2+
title: Hardcode `StandardField` names and use exact or customized names otherwise
3+
nav_order: 49
4+
parent: Decision Records
5+
status: accepted
6+
date: 2025-09-13
7+
---
8+
<!-- markdownlint-disable-next-line MD025 -->
9+
# Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)
10+
11+
## Context and Problem Statement
12+
13+
JabRef allows users to define custom fields and customizing `StandardField`s with arbitrary names and arbitrary casing. Users reported inconsistent casing of field names across the tabs in the details panes of the entry editor (Required/Optional/Other fields), within saved BibTeX files and preferences.
14+
Fields were partially forced to show in UI with a capital first letter by a UI method inside the `Field` model.
15+
A argument was made in issue [#116](https://github.com/JabRef/jabref/issues/116) about how to case the field names in the `.bib` file. It was then decided to always lowercase the field names, as BibTeX itself is case-insensitive in that matter, but convention in bibtex style is to lowercase it.
16+
There were complains, that this inconsistency [confuses users](https://github.com/JabRef/jabref/issues/10590) and makes it impossible to achieve a predictable, uniform presentation between UI and persisted data, especially when dealing with customized fields.
17+
18+
How should JabRef consistently determine the casing for field names in UI and persistence for both built‑in and custom fields?
19+
20+
## Decision Drivers
21+
22+
* Consistent user experience across all UI locations
23+
* Predictable persistence and round‑trip stability (UI ↔ preferences ↔ `.bib`)
24+
* Backward compatibility with existing libraries and preferences
25+
* Internationalization (i18n) and localization concerns for built‑in fields
26+
* Minimal complexity added to parsing/serialization logic
27+
* Avoid breaking community conventions for BibTeX/BibLaTeX (lowercase keys in files)
28+
* Avoid mixed casing rules per UI location
29+
* Minimize changes to bib files
30+
* Respect the choice of the user for casing of custom fields
31+
32+
## Considered Options
33+
34+
* Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)
35+
* Make all field names (including `StandardField`s) fully user‑configurable for display casing across UI and persistence
36+
* Normalize all field names to lowercase everywhere (UI, preferences, `.bib`)
37+
* Normalize all field names to title case in UI and in preferences; lowercase in `.bib`
38+
39+
## Decision Outcome
40+
41+
Chosen option: "Hardcode `StandardField` names and use exact or customized names otherwise," because
42+
43+
* Aligns with community conventions (lowercase keys in `.bib`), ensuring compatibility.
44+
* Provides a consistent and localized UI for built‑ins by using canonical, hardcoded display labels.
45+
* Respects users’ expectations for custom fields by preserving the casing they define everywhere in the UI and in preferences.
46+
* Minimizes behavioral surprises and avoids mixed casing rules per UI location.
47+
48+
## Pros and Cons of the Options
49+
50+
<!-- markdownlint-disable-next-line MD024 -->
51+
### Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)
52+
53+
<!-- markdownlint-disable-next-line MD004 -->>
54+
- For the build-in types (`StandardField`), the display names are hard-coded. Users cannot customize this. Optional/required can still be customized.
55+
<!-- markdownlint-disable-next-line MD004 -->>
56+
- Preserve exact user/customized names for non‑standard fields
57+
<!-- markdownlint-disable-next-line MD004 -->>
58+
- Serialize as customized (and lower-case as standard) to `.bib` file
59+
60+
* Good, because round‑trip: UI labels ↔ preferences ↔ UI remain stable.
61+
* Good, because built‑in labels can be localized predictably (title case or localized form).
62+
* Good, because consistent casing across entry editor tabs.
63+
* Good, because matches BibTeX convention for stored keys.
64+
* Good, because supports localization of displaying of `StandardField` names.
65+
* Good, because `.bib` files keep canonical lowercase keys in the default case. This matches common BibTeX/BibLaTeX practice.
66+
* Good, because decouples model (internal key) from UI (display label).
67+
* Bad, because users cannot change casing of built‑in field display names by using the entry customization.
68+
* Bad, because perceived as inconsistent at [#10590](https://github.com/JabRef/jabref/issues/10590).
69+
* Bad, because requires a clear separation of internal key vs. display label, which slightly increases conceptual complexity.
70+
* Bad, because migration needs to ensure older preferences do not inadvertently force lowercasing of custom fields in UI.
71+
72+
### Make all fields fully user‑configurable
73+
74+
This is option "Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)" with allowing customization of `StandardField`s.
75+
76+
* Good, because provides maximum flexibility for users: Users can change casing of built‑in field display names by using the entry customization.
77+
* Good, because perceived as consistent at [#10590](https://github.com/JabRef/jabref/issues/10590).
78+
* Bad, because increases settings complexity and risk of inconsistent UI and preferences.
79+
* Bad, because harder to localize built‑ins; can lead to team‑specific divergences.
80+
81+
### Normalize all field names to lowercase everywhere (UI, preferences, `.bib`)
82+
83+
* Good, because simplest to implement; fully consistent.
84+
* Bad, because Poor UX; clashes with common expectations for UI labels.
85+
* Bad, because Undermines localization and readability.
86+
87+
### Normalize all field names to title case in UI and in preferences; lowercase in `.bib`
88+
89+
* Good, because UI looks consistent and readable.
90+
* Bad, because ignores user intent for custom fields’ casing (less flexibility).
91+
* Bad, because preferences may mask underlying canonical keys, complicating tooling.

jabgui/src/main/java/org/jabref/gui/cleanup/CleanupPresetPanel.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.jabref.logic.cleanup.FieldFormatterCleanups;
1818
import org.jabref.logic.l10n.Localization;
1919
import org.jabref.model.database.BibDatabaseContext;
20+
import org.jabref.model.entry.field.FieldTextMapper;
2021
import org.jabref.model.entry.field.StandardField;
2122

2223
import com.airhacks.afterburner.views.ViewLoader;
@@ -65,7 +66,7 @@ private void init(CleanupPreferences cleanupPreferences, FilePreferences filePre
6566

6667
cleanUpRenamePDFonlyRelativePaths.disableProperty().bind(cleanUpRenamePDF.selectedProperty().not());
6768

68-
cleanUpUpgradeExternalLinks.setText(Localization.lang("Upgrade external PDF/PS links to use the '%0' field.", StandardField.FILE.getDisplayName()));
69+
cleanUpUpgradeExternalLinks.setText(Localization.lang("Upgrade external PDF/PS links to use the '%0' field.", FieldTextMapper.getDisplayName(StandardField.FILE)));
6970

7071
String currentPattern = Localization.lang("Filename format pattern (from preferences)")
7172
.concat(filePreferences.getFileNamePattern());

jabgui/src/main/java/org/jabref/gui/commonfxcontrols/FieldFormatterCleanupsPanel.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.jabref.logic.cleanup.Formatter;
2424
import org.jabref.logic.l10n.Localization;
2525
import org.jabref.model.entry.field.Field;
26+
import org.jabref.model.entry.field.FieldTextMapper;
2627

2728
import com.airhacks.afterburner.views.ViewLoader;
2829
import jakarta.inject.Inject;
@@ -63,7 +64,7 @@ private void setupTable() {
6364

6465
fieldColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getField()));
6566
new ValueTableCellFactory<FieldFormatterCleanup, Field>()
66-
.withText(Field::getDisplayName)
67+
.withText(FieldTextMapper::getDisplayName)
6768
.install(fieldColumn);
6869

6970
formatterColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getFormatter()));
@@ -73,9 +74,9 @@ private void setupTable() {
7374

7475
actionsColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getField()));
7576
new ValueTableCellFactory<FieldFormatterCleanup, Field>()
76-
.withGraphic(field -> IconTheme.JabRefIcons.DELETE_ENTRY.getGraphicNode())
77-
.withTooltip(field -> Localization.lang("Remove formatter for %0", field.getDisplayName()))
78-
.withOnMouseClickedEvent(item -> event -> viewModel.removeCleanup(cleanupsList.getSelectionModel().getSelectedItem()))
77+
.withGraphic(_ -> IconTheme.JabRefIcons.DELETE_ENTRY.getGraphicNode())
78+
.withTooltip(field -> Localization.lang("Remove formatter for %0", FieldTextMapper.getDisplayName(field)))
79+
.withOnMouseClickedEvent(_ -> _ -> viewModel.removeCleanup(cleanupsList.getSelectionModel().getSelectedItem()))
7980
.install(actionsColumn);
8081

8182
viewModel.selectedCleanupProperty().setValue(cleanupsList.getSelectionModel());
@@ -89,7 +90,7 @@ private void setupTable() {
8990

9091
private void setupCombos() {
9192
new ViewModelListCellFactory<Field>()
92-
.withText(Field::getDisplayName)
93+
.withText(FieldTextMapper::getDisplayName)
9394
.install(addableFields);
9495
addableFields.setConverter(FieldsUtil.FIELD_STRING_CONVERTER);
9596
addableFields.setOnKeyPressed(event -> {

jabgui/src/main/java/org/jabref/gui/commonfxcontrols/FieldFormatterCleanupsPanelViewModel.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,14 @@
2020
import org.jabref.logic.formatter.Formatters;
2121
import org.jabref.model.entry.field.Field;
2222
import org.jabref.model.entry.field.FieldFactory;
23+
import org.jabref.model.entry.field.FieldTextMapper;
2324

2425
public class FieldFormatterCleanupsPanelViewModel {
2526

2627
private final BooleanProperty cleanupsDisableProperty = new SimpleBooleanProperty();
2728
private final ListProperty<FieldFormatterCleanup> cleanupsListProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
2829
private final ObjectProperty<SelectionModel<FieldFormatterCleanup>> selectedCleanupProperty = new SimpleObjectProperty<>(new NoSelectionModel<>());
29-
private final ListProperty<Field> availableFieldsProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(FieldFactory.getCommonFields()), Comparator.comparing(Field::getDisplayName)));
30+
private final ListProperty<Field> availableFieldsProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(FieldFactory.getCommonFields()), Comparator.comparing(FieldTextMapper::getDisplayName)));
3031
private final ObjectProperty<Field> selectedFieldProperty = new SimpleObjectProperty<>();
3132
private final ListProperty<Formatter> availableFormattersProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(Formatters.getAll()), Comparator.comparing(Formatter::getName)));
3233
private final ObjectProperty<Formatter> selectedFormatterProperty = new SimpleObjectProperty<>();

jabgui/src/main/java/org/jabref/gui/consistency/ConsistencyCheckDialog.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.jabref.model.entry.BibEntryTypesManager;
3030
import org.jabref.model.entry.field.Field;
3131
import org.jabref.model.entry.field.FieldFactory;
32+
import org.jabref.model.entry.field.FieldTextMapper;
3233
import org.jabref.model.entry.field.SpecialField;
3334

3435
import com.airhacks.afterburner.views.ViewLoader;
@@ -172,7 +173,7 @@ protected void updateItem(String item, boolean empty) {
172173
.forEach(this::removeColumnWithUniformValue);
173174

174175
Arrays.stream(SpecialField.values())
175-
.map(SpecialField::getDisplayName)
176+
.map(FieldTextMapper::getDisplayName)
176177
.forEach(this::removeColumnByTitle);
177178
}
178179

jabgui/src/main/java/org/jabref/gui/consistency/ConsistencyCheckDialogViewModel.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.jabref.model.entry.BibEntryTypesManager;
3737
import org.jabref.model.entry.field.BibField;
3838
import org.jabref.model.entry.field.Field;
39+
import org.jabref.model.entry.field.FieldTextMapper;
3940
import org.jabref.model.entry.types.EntryType;
4041

4142
import org.jooq.lambda.Unchecked;
@@ -93,7 +94,7 @@ public List<String> getColumnNames() {
9394
List<String> result = new ArrayList<>(allReportedFields.size() + 2); // there are two extra columns
9495
result.add("Entry Type");
9596
result.add("CitationKey");
96-
allReportedFields.forEach(field -> result.add(field.getDisplayName().trim()));
97+
allReportedFields.forEach(field -> result.add(FieldTextMapper.getDisplayName(field).trim()));
9798
return result;
9899
}
99100

jabgui/src/main/java/org/jabref/gui/fieldeditors/CitationCountEditor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.jabref.logic.util.TaskExecutor;
2020
import org.jabref.model.entry.BibEntry;
2121
import org.jabref.model.entry.field.Field;
22+
import org.jabref.model.entry.field.FieldTextMapper;
2223

2324
import com.airhacks.afterburner.injection.Injector;
2425
import com.airhacks.afterburner.views.ViewLoader;
@@ -58,7 +59,7 @@ public CitationCountEditor(Field field,
5859
textField.textProperty().bindBidirectional(viewModel.textProperty());
5960

6061
fetchCitationCountButton.setTooltip(
61-
new Tooltip(Localization.lang("Look up %0", field.getDisplayName())));
62+
new Tooltip(Localization.lang("Look up %0", FieldTextMapper.getDisplayName(field))));
6263
textField.initContextMenu(new DefaultMenu(textField), preferences.getKeyBindingRepository());
6364
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
6465
}

0 commit comments

Comments
 (0)