Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e2f526d
Prototype
calixtus Sep 12, 2025
344cdb5
Add ADR
calixtus Sep 13, 2025
9e037d8
Modify ADR
calixtus Sep 13, 2025
adec9ec
Fix tests
calixtus Sep 13, 2025
e442cc1
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 13, 2025
b1b9a8e
Fix merge conflict
calixtus Sep 13, 2025
29054a9
CHANGELOG.md
calixtus Sep 13, 2025
33013d8
Fix APA fields
calixtus Sep 13, 2025
dcbf1cb
Refine ADR
koppor Sep 13, 2025
f073260
Merge branch 'fix-inconsistent-casing' of github.com:JabRef/jabref in…
koppor Sep 13, 2025
0988273
typo
calixtus Sep 13, 2025
adf2a35
Merge remote-tracking branch 'upstream/main' into fix-inconsistent-ca…
calixtus Sep 13, 2025
7407839
Code formatting
calixtus Sep 13, 2025
33ebccd
Merge remote-tracking branch 'upstream/main' into fix-inconsistent-ca…
calixtus Sep 13, 2025
bf06837
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 13, 2025
54bfeab
Remove capitalization from orfield
calixtus Sep 13, 2025
202aa53
Merge remote-tracking branch 'upstream/fix-inconsistent-casing' into …
calixtus Sep 13, 2025
ad6a119
Comment to ADR
calixtus Sep 13, 2025
5d11e29
Remove forced lowercase of field names in bib file
calixtus Sep 14, 2025
4a7d8ef
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 14, 2025
6b3627a
Fix tests
calixtus Sep 14, 2025
d92153f
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 14, 2025
7aa9904
Fix more tests
calixtus Sep 14, 2025
6495eb0
Add special case for OpenOffice/LibreOffice
calixtus Sep 14, 2025
fbbc389
Rewrite
calixtus Sep 14, 2025
4e96ca9
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 14, 2025
bb8c653
Suggestions
calixtus Sep 14, 2025
5c11c9e
Merge branch 'main' into fix-inconsistent-casing
calixtus Sep 14, 2025
d741bac
Fix tests
calixtus Sep 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- 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)
- 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)
- 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)
- We fixed an issue where field names were inconsistently capitalized. [#10590](https://github.com/JabRef/jabref/issues/10590)
- We fixed an issue where "Copy to" was enabled even if no other library was opened. [#13280](https://github.com/JabRef/jabref/pull/13280)
- We fixed an issue where the groups were still displayed after closing all libraries. [#13382](https://github.com/JabRef/jabref/issues/13382)
- 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)
Expand Down
91 changes: 91 additions & 0 deletions docs/decisions/0049-hardcode-fieldnames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: Hardcode `StandardField` names and use exact or customized names otherwise
nav_order: 49
parent: Decision Records
status: accepted
date: 2025-09-13
---
<!-- markdownlint-disable-next-line MD025 -->
# Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)

## Context and Problem Statement

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.
Fields were partially forced to show in UI with a capital first letter by a UI method inside the `Field` model.
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.
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.

How should JabRef consistently determine the casing for field names in UI and persistence for both built‑in and custom fields?

## Decision Drivers

* Consistent user experience across all UI locations
* Predictable persistence and round‑trip stability (UI ↔ preferences ↔ `.bib`)
* Backward compatibility with existing libraries and preferences
* Internationalization (i18n) and localization concerns for built‑in fields
* Minimal complexity added to parsing/serialization logic
* Avoid breaking community conventions for BibTeX/BibLaTeX (lowercase keys in files)
* Avoid mixed casing rules per UI location
* Minimize changes to bib files
* Respect the choice of the user for casing of custom fields

## Considered Options

* Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)
* Make all field names (including `StandardField`s) fully user‑configurable for display casing across UI and persistence
* Normalize all field names to lowercase everywhere (UI, preferences, `.bib`)
* Normalize all field names to title case in UI and in preferences; lowercase in `.bib`

## Decision Outcome

Chosen option: "Hardcode `StandardField` names and use exact or customized names otherwise," because

* Aligns with community conventions (lowercase keys in `.bib`), ensuring compatibility.
* Provides a consistent and localized UI for built‑ins by using canonical, hardcoded display labels.
* Respects users’ expectations for custom fields by preserving the casing they define everywhere in the UI and in preferences.
* Minimizes behavioral surprises and avoids mixed casing rules per UI location.

## Pros and Cons of the Options

<!-- markdownlint-disable-next-line MD024 -->
### Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)

<!-- markdownlint-disable-next-line MD004 -->>
- For the build-in types (`StandardField`), the display names are hard-coded. Users cannot customize this. Optional/required can still be customized.
<!-- markdownlint-disable-next-line MD004 -->>
- Preserve exact user/customized names for non‑standard fields
<!-- markdownlint-disable-next-line MD004 -->>
- Serialize as customized (and lower-case as standard) to `.bib` file

* Good, because round‑trip: UI labels ↔ preferences ↔ UI remain stable.
* Good, because built‑in labels can be localized predictably (title case or localized form).
* Good, because consistent casing across entry editor tabs.
* Good, because matches BibTeX convention for stored keys.
* Good, because supports localization of displaying of `StandardField` names.
* Good, because `.bib` files keep canonical lowercase keys in the default case. This matches common BibTeX/BibLaTeX practice.
* Good, because decouples model (internal key) from UI (display label).
* Bad, because users cannot change casing of built‑in field display names by using the entry customization.
* Bad, because perceived as inconsistent at [#10590](https://github.com/JabRef/jabref/issues/10590).
* Bad, because requires a clear separation of internal key vs. display label, which slightly increases conceptual complexity.
* Bad, because migration needs to ensure older preferences do not inadvertently force lowercasing of custom fields in UI.

### Make all fields fully user‑configurable

This is option "Hardcode `StandardField` names and use exact or customized names otherwise (disallow customization of `StandardField`s)" with allowing customization of `StandardField`s.

* Good, because provides maximum flexibility for users: Users can change casing of built‑in field display names by using the entry customization.
* Good, because perceived as consistent at [#10590](https://github.com/JabRef/jabref/issues/10590).
* Bad, because increases settings complexity and risk of inconsistent UI and preferences.
* Bad, because harder to localize built‑ins; can lead to team‑specific divergences.

### Normalize all field names to lowercase everywhere (UI, preferences, `.bib`)

* Good, because simplest to implement; fully consistent.
* Bad, because Poor UX; clashes with common expectations for UI labels.
* Bad, because Undermines localization and readability.

### Normalize all field names to title case in UI and in preferences; lowercase in `.bib`

* Good, because UI looks consistent and readable.
* Bad, because ignores user intent for custom fields’ casing (less flexibility).
* Bad, because preferences may mask underlying canonical keys, complicating tooling.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.jabref.logic.cleanup.FieldFormatterCleanups;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.field.FieldTextMapper;
import org.jabref.model.entry.field.StandardField;

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

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

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

String currentPattern = Localization.lang("Filename format pattern (from preferences)")
.concat(filePreferences.getFileNamePattern());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.jabref.logic.cleanup.Formatter;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldTextMapper;

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

fieldColumn.setCellValueFactory(cellData -> new ReadOnlyObjectWrapper<>(cellData.getValue().getField()));
new ValueTableCellFactory<FieldFormatterCleanup, Field>()
.withText(Field::getDisplayName)
.withText(FieldTextMapper::getDisplayName)
.install(fieldColumn);

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

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

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

private void setupCombos() {
new ViewModelListCellFactory<Field>()
.withText(Field::getDisplayName)
.withText(FieldTextMapper::getDisplayName)
.install(addableFields);
addableFields.setConverter(FieldsUtil.FIELD_STRING_CONVERTER);
addableFields.setOnKeyPressed(event -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
import org.jabref.logic.formatter.Formatters;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;
import org.jabref.model.entry.field.FieldTextMapper;

public class FieldFormatterCleanupsPanelViewModel {

private final BooleanProperty cleanupsDisableProperty = new SimpleBooleanProperty();
private final ListProperty<FieldFormatterCleanup> cleanupsListProperty = new SimpleListProperty<>(FXCollections.observableArrayList());
private final ObjectProperty<SelectionModel<FieldFormatterCleanup>> selectedCleanupProperty = new SimpleObjectProperty<>(new NoSelectionModel<>());
private final ListProperty<Field> availableFieldsProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(FieldFactory.getCommonFields()), Comparator.comparing(Field::getDisplayName)));
private final ListProperty<Field> availableFieldsProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(FieldFactory.getCommonFields()), Comparator.comparing(FieldTextMapper::getDisplayName)));
private final ObjectProperty<Field> selectedFieldProperty = new SimpleObjectProperty<>();
private final ListProperty<Formatter> availableFormattersProperty = new SimpleListProperty<>(new SortedList<>(FXCollections.observableArrayList(Formatters.getAll()), Comparator.comparing(Formatter::getName)));
private final ObjectProperty<Formatter> selectedFormatterProperty = new SimpleObjectProperty<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;
import org.jabref.model.entry.field.FieldTextMapper;
import org.jabref.model.entry.field.SpecialField;

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

Arrays.stream(SpecialField.values())
.map(SpecialField::getDisplayName)
.map(FieldTextMapper::getDisplayName)
.forEach(this::removeColumnByTitle);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.jabref.model.entry.BibEntryTypesManager;
import org.jabref.model.entry.field.BibField;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldTextMapper;
import org.jabref.model.entry.types.EntryType;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.jabref.logic.util.TaskExecutor;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldTextMapper;

import com.airhacks.afterburner.injection.Injector;
import com.airhacks.afterburner.views.ViewLoader;
Expand Down Expand Up @@ -58,7 +59,7 @@ public CitationCountEditor(Field field,
textField.textProperty().bindBidirectional(viewModel.textProperty());

fetchCitationCountButton.setTooltip(
new Tooltip(Localization.lang("Look up %0", field.getDisplayName())));
new Tooltip(Localization.lang("Look up %0", FieldTextMapper.getDisplayName(field))));
textField.initContextMenu(new DefaultMenu(textField), preferences.getKeyBindingRepository());
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
}
Expand Down
Loading
Loading