This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new eb1582adde Add format mask support to boolean fields and update 
documentation, f… (#8557)
eb1582adde is described below

commit eb1582adde9b0c99dca7a46ebcabe76ad4ce48de
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Thu Sep 24 12:50:48 2026 +0200

    Add format mask support to boolean fields and update documentation, f… 
(#8557)
    
    * Add format mask support to boolean fields and update documentation, fixes 
#5958
    
    * fix remarks
---
 core/src/main/java/org/apache/hop/core/Const.java  |  11 +
 .../apache/hop/core/row/value/ValueMetaBase.java   |  83 +++-
 .../hop/core/row/value/ValueMetaBaseTest.java      | 149 ++++++++
 .../ROOT/pages/pipeline/formatting-values.adoc     |  57 +++
 .../pages/pipeline/transforms/textfileoutput.adoc  |   5 +-
 .../snippets/field-metadata/field-columns.adoc     |   3 +-
 .../pages/snippets/hop-concepts/data-types.adoc    |  33 ++
 .../transforms/0118-boolean-format-read-back.hpl   | 261 +++++++++++++
 .../0118-boolean-format-text-file-output.hpl       | 417 +++++++++++++++++++++
 .../transforms/datasets/golden-boolean-formats.csv |   5 +
 .../transforms/files/boolean-formats.csv           |   5 +
 .../transforms/main-0118-boolean-format.hwf        | 115 ++++++
 .../metadata/dataset/golden-boolean-formats.json   |  63 ++++
 .../0118-boolean-format-read-back UNIT.json        |  52 +++
 .../concatfields/ConcatFieldsDialog.java           |   4 +-
 .../hop/ui/core/dialog/EnterValueDialog.java       |   9 +
 .../org/apache/hop/ui/core/widget/TableView.java   |   1 +
 17 files changed, 1267 insertions(+), 6 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/Const.java 
b/core/src/main/java/org/apache/hop/core/Const.java
index 33fe4cbf5e..e99cc8509c 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -356,6 +356,9 @@ public class Const {
   /** An array of number conversion formats */
   private static String[] numberFormats;
 
+  /** The Boolean conversion formats: the text for true and for false, 
separated by a slash */
+  private static final String[] BOOLEAN_FORMATS = {"true/false", "Y/N", "1/0", 
"yes/no"};
+
   /**
    * Generalized date/time format: Wherever dates are used, date and time 
values are organized from
    * the most to the least significant. see also method 
StringUtil.getFormattedDateTime()
@@ -2994,6 +2997,14 @@ public class Const {
     return (String[]) ArrayUtils.addAll(Const.getDateFormats(), 
Const.getNumberFormats());
   }
 
+  /**
+   * @return The Boolean conversion formats, each holding the text for true 
and for false separated
+   *     by a slash (for example {@code Y/N})
+   */
+  public static String[] getBooleanFormats() {
+    return BOOLEAN_FORMATS.clone();
+  }
+
   /**
    * Return the input string trimmed as specified.
    *
diff --git 
a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java 
b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
index 652a9d0583..5fb5417fa1 100644
--- a/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
+++ b/core/src/main/java/org/apache/hop/core/row/value/ValueMetaBase.java
@@ -1698,10 +1698,44 @@ public class ValueMetaBase implements IValueMeta {
 
   // BOOLEAN + STRING
 
+  /**
+   * A Boolean format mask holds the text for true and the text for false, 
separated by a single
+   * slash, for example {@code true/false}, {@code Y/N} or {@code 1/0}. Spaces 
around either text
+   * are not part of it: {@code Ja / Nein} is {@code Ja} and {@code Nein}.
+   *
+   * <p>A mask with no slash, or with more than one (a date mask like {@code 
yyyy/MM/dd}), is not a
+   * Boolean mask. Neither is one with an empty side, or with the same text on 
both sides ignoring
+   * case: reading ignores case, so {@code Yes/yes} could not tell the two 
apart.
+   *
+   * @param mask the conversion mask
+   * @return the text for true and the text for false, or null when the mask 
is not a Boolean mask
+   */
+  static String[] getBooleanMaskTexts(String mask) {
+    if (mask == null) {
+      return null;
+    }
+    int slash = mask.indexOf('/');
+    if (slash < 0 || mask.indexOf('/', slash + 1) >= 0) {
+      return null;
+    }
+    String trueText = mask.substring(0, slash).trim();
+    String falseText = mask.substring(slash + 1).trim();
+    if (trueText.isEmpty() || falseText.isEmpty() || 
trueText.equalsIgnoreCase(falseText)) {
+      return null;
+    }
+    return new String[] {trueText, falseText};
+  }
+
   protected String convertBooleanToString(Boolean bool) {
     if (bool == null) {
       return null;
     }
+    String[] texts = getBooleanMaskTexts(conversionMask);
+    if (texts != null) {
+      return bool ? texts[0] : texts[1];
+    }
+    // Without a mask the length decides, a legacy rule kept for compatibility
+    //
     if (length >= 3) {
       return bool ? "true" : CONST_FALSE;
     } else {
@@ -1720,6 +1754,38 @@ public class ValueMetaBase implements IValueMeta {
         || "1".equals(string);
   }
 
+  /**
+   * Converts a String to a Boolean, first matching the true and false text of 
a Boolean conversion
+   * mask (ignoring case and surrounding spaces). Text that matches neither 
falls back to {@link
+   * #convertStringToBoolean(String)}.
+   *
+   * <p>The mask is this value's own or, when that is not a Boolean mask, the 
one of its conversion
+   * metadata: converting typed text to a Boolean value goes through a String 
value whose conversion
+   * metadata is the Boolean, the way a date pattern is found for dates.
+   *
+   * @param string the string to convert
+   * @return the Boolean, or null for an empty string
+   */
+  protected Boolean convertMaskedStringToBoolean(String string) {
+    if (Utils.isEmpty(string)) {
+      return null;
+    }
+    String[] texts = getBooleanMaskTexts(conversionMask);
+    if (texts == null && conversionMetadata != null) {
+      texts = getBooleanMaskTexts(conversionMetadata.getConversionMask());
+    }
+    if (texts != null) {
+      String text = string.trim();
+      if (text.equalsIgnoreCase(texts[0])) {
+        return true;
+      }
+      if (text.equalsIgnoreCase(texts[1])) {
+        return false;
+      }
+    }
+    return convertStringToBoolean(string);
+  }
+
   // BOOLEAN + NUMBER
 
   protected Double convertBooleanToNumber(Boolean bool) {
@@ -2703,12 +2769,12 @@ public class ValueMetaBase implements IValueMeta {
         };
       case TYPE_STRING:
         return switch (storageType) {
-          case STORAGE_TYPE_NORMAL -> convertStringToBoolean(trim((String) 
object));
+          case STORAGE_TYPE_NORMAL -> 
convertMaskedStringToBoolean(trim((String) object));
           case STORAGE_TYPE_BINARY_STRING ->
-              convertStringToBoolean(
+              convertMaskedStringToBoolean(
                   trim((String) convertBinaryStringToNativeType((byte[]) 
object)));
           case STORAGE_TYPE_INDEXED ->
-              convertStringToBoolean(trim((String) index[(Integer) object]));
+              convertMaskedStringToBoolean(trim((String) index[(Integer) 
object]));
           default ->
               throw new HopValueException(
                   this
@@ -4730,6 +4796,17 @@ public class ValueMetaBase implements IValueMeta {
                 (getConversionMask() != null
                         && 
getConversionMask().equals(storageMetadata.getConversionMask()))
                     || (getConversionMask() == null && 
storageMetadata.getConversionMask() == null);
+          } else if (isBoolean()) {
+            // The Boolean mask decides the text, and without one the length 
does (Y/N or
+            // true/false). The stored bytes can only be written as they are 
if both agree.
+            //
+            String[] texts = getBooleanMaskTexts(getConversionMask());
+            String[] storageTexts = 
getBooleanMaskTexts(storageMetadata.getConversionMask());
+            if (texts == null && storageTexts == null) {
+              identicalFormat = getLength() == storageMetadata.getLength();
+            } else {
+              identicalFormat = Arrays.equals(texts, storageTexts);
+            }
           } else if (isNumeric()) {
             // Check the lengths first
             //
diff --git 
a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java 
b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
index a5bc242c96..b590e398b3 100644
--- a/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
+++ b/core/src/test/java/org/apache/hop/core/row/value/ValueMetaBaseTest.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
@@ -303,6 +304,154 @@ class ValueMetaBaseTest {
     assertFalse(ValueMetaBase.convertStringToBoolean("NOT false"));
   }
 
+  @Test
+  void testBooleanToStringWithoutMaskFollowsLength() throws HopValueException {
+    // Without a mask, the length decides (#5958): 3 or more prints 
true/false, anything else Y/N
+    assertEquals("Y", new ValueMetaBoolean("b").getString(true));
+    assertEquals("N", new ValueMetaBoolean("b", 1, -1).getString(false));
+    assertEquals("true", new ValueMetaBoolean("b", 4, -1).getString(true));
+    assertEquals("false", new ValueMetaBoolean("b", 4, -1).getString(false));
+  }
+
+  @Test
+  void testBooleanToStringWithMask() throws HopValueException {
+    ValueMetaBoolean shortField = new ValueMetaBoolean("b");
+    ValueMetaBoolean longField = new ValueMetaBoolean("b", 4, -1);
+    for (String mask : Const.getBooleanFormats()) {
+      String[] texts = mask.split("/");
+      shortField.setConversionMask(mask);
+      longField.setConversionMask(mask);
+      // The mask wins over the length, so both fields print the same
+      assertEquals(texts[0], shortField.getString(true), mask);
+      assertEquals(texts[1], shortField.getString(false), mask);
+      assertEquals(texts[0], longField.getString(true), mask);
+      assertEquals(texts[1], longField.getString(false), mask);
+    }
+
+    ValueMetaBoolean custom = new ValueMetaBoolean("b");
+    custom.setConversionMask("Ja/Nee");
+    assertEquals("Ja", custom.getString(true));
+    assertEquals("Nee", custom.getString(false));
+    assertNull(custom.getString(null));
+  }
+
+  @Test
+  void testBooleanToBinaryStringWithMask() throws HopValueException {
+    ValueMetaBoolean field = new ValueMetaBoolean("b");
+    field.setConversionMask("yes/no");
+    assertArrayEquals("yes".getBytes(), field.getBinaryString(true));
+    assertArrayEquals("no".getBytes(), field.getBinaryString(false));
+  }
+
+  @Test
+  void testNonBooleanMaskIsIgnored() throws HopValueException {
+    ValueMetaBoolean field = new ValueMetaBoolean("b");
+    // Yes/yes: the same word twice ignoring case, which reading could not 
tell apart
+    for (String mask : new String[] {"yyyy/MM/dd", "#.##", "/N", "Y/", "/", " 
/ ", "Yes/yes"}) {
+      field.setConversionMask(mask);
+      assertEquals("Y", field.getString(true), mask);
+      assertEquals("N", field.getString(false), mask);
+    }
+  }
+
+  @Test
+  void testStringToBooleanWithMask() throws HopValueException {
+    ValueMetaString string = new ValueMetaString("s");
+    string.setConversionMask("Ja/Nee");
+    assertTrue(string.getBoolean("Ja"));
+    assertTrue(string.getBoolean("ja"));
+    assertFalse(string.getBoolean("NEE"));
+    // Text outside the mask falls back to the standard rules
+    assertTrue(string.getBoolean("Y"));
+    assertTrue(string.getBoolean("true"));
+    assertFalse(string.getBoolean("maybe"));
+    assertNull(string.getBoolean(""));
+
+    // A mask whose true text is not in the standard rules, e.g. T/F
+    string.setConversionMask("T/F");
+    assertTrue(string.getBoolean("t"));
+    assertFalse(string.getBoolean("F"));
+  }
+
+  @Test
+  void testBooleanMaskTextsAreTrimmed() throws HopValueException {
+    ValueMetaBoolean field = new ValueMetaBoolean("b");
+    field.setConversionMask("Ja / Nein");
+    assertEquals("Ja", field.getString(true));
+    assertEquals("Nein", field.getString(false));
+
+    // What was written reads back, also once a reader trimmed it
+    ValueMetaString string = new ValueMetaString("s");
+    string.setConversionMask("Ja / Nein");
+    assertTrue(string.getBoolean("Ja"));
+    assertFalse(string.getBoolean("Nein"));
+    assertFalse(string.getBoolean(" nein "));
+  }
+
+  /**
+   * Typed text becomes a Boolean through a String value whose conversion 
metadata is the Boolean:
+   * the Enter Value dialog, a condition's value, a sorted table cell. The 
mask is on the Boolean.
+   */
+  @Test
+  void testStringToBooleanUsesTheMaskOfTheConversionMetadata() throws 
HopValueException {
+    for (String[] mask :
+        new String[][] {{"Ja/Nee", "Ja", "Nee"}, {"T/F", "T", "F"}, {"on/off", 
"on", "off"}}) {
+      ValueMetaBoolean booleanMeta = new ValueMetaBoolean("b");
+      booleanMeta.setConversionMask(mask[0]);
+      ValueMetaString stringMeta = new ValueMetaString("s");
+      stringMeta.setConversionMetadata(booleanMeta);
+
+      assertEquals(Boolean.TRUE, 
stringMeta.convertDataUsingConversionMetaData(mask[1]), mask[0]);
+      assertEquals(Boolean.FALSE, 
stringMeta.convertDataUsingConversionMetaData(mask[2]), mask[0]);
+    }
+  }
+
+  /**
+   * A Boolean read lazily keeps the bytes of the file. They are only written 
as they are while the
+   * format still matches: another mask, or another length without one, 
converts them.
+   */
+  @Test
+  void testBinaryStringBooleanFollowsAChangedMask() throws Exception {
+    ValueMetaBoolean field = new ValueMetaBoolean("b");
+    field.setConversionMask("Y/N");
+    IValueMeta storage = ValueMetaFactory.cloneValueMeta(field, 
IValueMeta.TYPE_STRING);
+    field.setStorageMetadata(storage);
+    field.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+
+    // Same mask as when it was read: the bytes go out unchanged
+    byte[] yes = "Y".getBytes();
+    assertSame(yes, field.getBinaryString(yes));
+
+    field.setConversionMask("Ja/Nee");
+    assertArrayEquals("Ja".getBytes(), field.getBinaryString("Y".getBytes()));
+    assertArrayEquals("Nee".getBytes(), field.getBinaryString("N".getBytes()));
+
+    // No mask on either side: the length decides the text, so a new length 
converts too
+    ValueMetaBoolean read = new ValueMetaBoolean("b", 1, -1);
+    IValueMeta readStorage = ValueMetaFactory.cloneValueMeta(read, 
IValueMeta.TYPE_STRING);
+    read.setStorageMetadata(readStorage);
+    read.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    assertSame(yes, read.getBinaryString(yes));
+
+    ValueMetaBoolean longer = new ValueMetaBoolean("b", 5, -1);
+    longer.setStorageMetadata(readStorage);
+    longer.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+    assertArrayEquals("true".getBytes(), 
longer.getBinaryString("Y".getBytes()));
+  }
+
+  @Test
+  void testConvertDataFromStringToBooleanWithMask() throws Exception {
+    ValueMetaBoolean target = new ValueMetaBoolean("b");
+    target.setConversionMask("on/off");
+    IValueMeta convertMeta = ValueMetaFactory.cloneValueMeta(target, 
IValueMeta.TYPE_STRING);
+    assertEquals(
+        Boolean.TRUE,
+        target.convertDataFromString("on", convertMeta, null, null, 
IValueMeta.TRIM_TYPE_BOTH));
+    assertEquals(
+        Boolean.FALSE,
+        target.convertDataFromString(" OFF ", convertMeta, null, null, 
IValueMeta.TRIM_TYPE_BOTH));
+  }
+
   @Test
   void testConvertDataFromStringToString() throws HopValueException {
     ValueMetaBase inValueMetaString = new ValueMetaString();
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
index 3d17cdcc30..ce87f0c7d6 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/formatting-values.adoc
@@ -194,6 +194,63 @@ Fixes, in order of preference:
 When you *write* dates to text, CSV, JSON or logs from a DST-observing JVM, 
use a mask with an offset, or set the output field's Date Time Zone to `UTC`.
 Otherwise a reader in another zone — or the same pipeline after a DST weekend 
— will not reconstruct the instant you had.
 
+== Boolean formats
+
+A Boolean format is the text for true, a slash, and the text for false.
+It is used both to print a Boolean and to read one from text.
+
+[options="header"]
+|===
+|Format|true|false
+|`true/false`|`true`|`false`
+|`Y/N`|`Y`|`N`
+|`1/0`|`1`|`0`
+|`yes/no`|`yes`|`no`
+|===
+
+Without a format, the Length of the field decides how a Boolean is printed.
+See xref:concepts.adoc#boolean-type[Boolean] for the details.
+
+[[boolean-own-format]]
+=== Your own true and false text
+
+The list above only holds suggestions.
+You can type any pair into the Format column, so a file can use the words of 
its readers' language:
+
+[options="header"]
+|===
+|Format|true|false
+|`Ja/Nee`|`Ja`|`Nee`
+|`Ja/Nein`|`Ja`|`Nein`
+|`Oui/Non`|`Oui`|`Non`
+|`Sí/No`|`Sí`|`No`
+|`on/off`|`on`|`off`
+|===
+
+For example, to write German Booleans with 
xref:pipeline/transforms/textfileoutput.adoc[Text File Output]:
+
+. Open the *Fields* tab and click *Get Fields*.
+. Check that the *Type* of the field is `Boolean`.
+Text File Output does not convert types, so a field that arrives as a String 
has to be converted first, for example with 
xref:pipeline/transforms/selectvalues.adoc[Select Values].
+. Type `Ja/Nein` in the *Format* column.
+
+Every `true` is then written as `Ja` and every `false` as `Nein`, whatever 
format or length the field had before.
+A null stays empty.
+
+To read such a file again, give the field the same Format in 
xref:pipeline/transforms/textfileinput.adoc[Text File Input] or in the 
Meta-data tab of xref:pipeline/transforms/selectvalues.adoc[Select Values].
+Reading ignores case, so `ja`, `JA` and `Ja` are all true.
+Text that matches neither word falls back to the standard rules: `Y`, `yes`, 
`true` and `1` are true, anything else is false.
+
+Keep these rules in mind when you define your own format:
+
+* Use exactly one slash: the text before it means true, the text after it 
means false.
+Neither word can contain a slash, and a mask with more than one slash (such as 
the date mask `yyyy/MM/dd`) is not treated as a Boolean format.
+* Both words must be non-empty.
+* Spaces around the slash are not part of the words: `Ja / Nein` writes `Ja` 
and `Nein`.
+* Pick two words that differ in more than upper and lower case.
+Reading ignores case, so a format like `Yes/yes` could never tell true from 
false and is not treated as a Boolean format.
+* Text File Output ignores all formats when *Fast data dump (no formatting)* 
is enabled, and then writes `true` or `false`.
+
 == Rounding
 
 include::../snippets/rounding-types.adoc[]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
index a35e1a7aa5..6a14d2e6d5 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/textfileoutput.adoc
@@ -99,6 +99,7 @@ To use Unicode specify UTF-8 or UTF-16.
 Note: Only one file is placed in a single compressed archive.
 |Right pad fields|Add spaces to the end of the fields (or remove characters at 
the end) until they have the length specific on the Fields tab.
 |Fast data dump (no formatting)|Improves the performance when dumping large 
amounts of data to a text file by not including any formatting information.
+Field formats are ignored, so a Boolean is written as `true` or `false`.
 |Split every ... rows|Specify a number of rows to split the file into chunks 
of that many rows. 
 |Add Ending line of file|Specify an alternate ending row to the output file.
 |===
@@ -114,7 +115,9 @@ The table below describes each of the options for 
configuring the field properti
 |Schema Definition|Name of the 
xref:metadata-types/static-schema-definition.adoc[Schema Definition] that we 
want to reference.
 |Name|The name of the field.
 |Type|Data type of the field. This can be either String, Date or Number.
-|Format|Select a format to apply to the field value. Only applies to numbers 
and dates.
+|Format|Select a format to apply to the field value. Applies to numbers, dates 
and Booleans.
+For a Boolean, the format is the text for true, a slash and the text for 
false, so `Ja/Nein` writes German words instead of `Y` / `N`.
+You can type your own pair, see 
xref:pipeline/formatting-values.adoc#boolean-own-format[Your own true and false 
text].
 See xref:pipeline/formatting-values.adoc[Formatting numbers and dates].
 |Length a| The length option depends on the field type:
 
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
index 0fa8423835..e519630019 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/snippets/field-metadata/field-columns.adoc
@@ -22,12 +22,13 @@ They do *not* change the value stored in a database.
 [options="header"]
 |===
 |Column|Description
-|Format / Conversion mask|Number or date pattern used to parse and print the 
value.
+|Format / Conversion mask|Number, date or Boolean pattern used to parse and 
print the value.
 See xref:pipeline/formatting-values.adoc[Formatting numbers and dates].
 |Length|String: maximum characters.
 Number / BigNumber: significant digits.
 Integer: display width (leading zeros when writing).
 Date: length of the printed string (for example `4` prints only the year).
+Boolean without a Format: `3` or more prints `true` / `false`, anything else 
prints `Y` / `N`.
 Use `-1` when unset.
 |Precision|Decimal digits after the radix for Number and BigNumber.
 Not used for String, Date or Boolean.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
index 7b10aeb891..5ee1754cea 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/snippets/hop-concepts/data-types.adoc
@@ -56,6 +56,39 @@ A common issue is the decimal separator, which differs 
between environments: an
 
 See xref:pipeline/formatting-values.adoc[Formatting numbers and dates] for 
Format, Length, Precision, Decimal, Group and Currency.
 
+[[boolean-type]]
+=== Boolean
+
+A Boolean field holds `true`, `false` or null.
+
+*Reading text as a Boolean*
+
+- Without a Format, `Y`, `yes`, `true` (in any case) and `1` are read as true. 
Any other text is read as false, and empty text is null.
+- With a Boolean Format such as `on/off`, text that matches the true or the 
false part of the mask (ignoring case) is read as that value.
+Text that matches neither part falls back to the rules above.
+
+*Writing a Boolean as text*
+
+This applies wherever Hop turns a Boolean into text: Write to log, the preview 
grid, text files, Concat fields and so on.
+
+- With a Boolean Format, Hop prints the true or the false part of the mask.
+- Without a Format, the field's *Length* decides: a length of 3 or more prints 
`true` / `false`, any other length (including no length) prints `Y` / `N`.
+
+A Boolean Format is the text for true, a slash, and the text for false, for 
example `true/false`, `Y/N`, `1/0` or `yes/no`.
+The mask must contain exactly one slash, so neither text can contain a slash 
itself.
+Spaces around the slash are ignored, and the two texts must differ in more 
than upper and lower case.
+The Format column offers the common masks for Boolean fields, and you can type 
any other pair, for example `Ja/Nein` to write German words.
+See xref:pipeline/formatting-values.adoc#boolean-own-format[Your own true and 
false text].
+
+*Why two Boolean fields can print differently*
+
+The length is often set for you rather than by you.
+Converting a String field to a Boolean, for example in the Meta-data tab of 
xref:pipeline/transforms/selectvalues.adoc[Select Values], keeps the String's 
length.
+A `true`/`false` column read from a database with length 4 therefore becomes a 
Boolean of length 4 and prints `true`, while a Boolean read by JSON Input has 
no length and prints `Y`.
+Both values are the same `true`.
+
+Set a Boolean Format on the fields to get the same text everywhere, whatever 
their length.
+
 === Timestamp
 
 A Timestamp value can be created from a Date, String, Number, or another 
Timestamp.
diff --git a/integration-tests/transforms/0118-boolean-format-read-back.hpl 
b/integration-tests/transforms/0118-boolean-format-read-back.hpl
new file mode 100644
index 0000000000..523149143e
--- /dev/null
+++ b/integration-tests/transforms/0118-boolean-format-read-back.hpl
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <pipeline_version/>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters/>
+    <name>0118-boolean-format-read-back</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Issue #5958: reads the file written by 
0118-boolean-format-text-file-output as plain text, so the unit test sees 
exactly what Text File Output wrote for each Boolean format.</description>
+    <extended_description/>
+    <created_user>-</created_user>
+    <modified_user>-</modified_user>
+    <created_date>2026/09/23 12:00:00.000</created_date>
+    <modified_date>2026/09/23 12:00:00.000</modified_date>
+  </info>
+  <transform>
+    <type>TextFileInput2</type>
+    <name>Read written text</name>
+    <file_type>CSV</file_type>
+    <compression>None</compression>
+    <separator>;</separator>
+    <enclosure>"</enclosure>
+    <enclosure_breaks>N</enclosure_breaks>
+    <escapechar/>
+    <header>Y</header>
+    <prependFileName>N</prependFileName>
+    <nr_headerlines>1</nr_headerlines>
+    <footer>N</footer>
+    <nr_footerlines>1</nr_footerlines>
+    <line_wrapped>N</line_wrapped>
+    <nr_wraps>1</nr_wraps>
+    <layout_paged>N</layout_paged>
+    <nr_lines_per_page>80</nr_lines_per_page>
+    <nr_lines_doc_header>0</nr_lines_doc_header>
+    <noempty>Y</noempty>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <rownum_field/>
+    <rownumByFile>N</rownumByFile>
+    <format>mixed</format>
+    <encoding>UTF-8</encoding>
+    <limit>0</limit>
+    <date_format_lenient>Y</date_format_lenient>
+    <date_format_locale>en_US</date_format_locale>
+    <length>Characters</length>
+    <filters/>
+    <error_count_field/>
+    <error_fields_field/>
+    <error_text_field/>
+    <error_line_skipped>N</error_line_skipped>
+    <schema_definition/>
+    <ignore_fields>N</ignore_fields>
+    <shortFileFieldName/>
+    <extensionFieldName/>
+    <pathFieldName/>
+    <sizeFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <files>
+      <file>
+        <name>${java.io.tmpdir}/0118/boolean-formats-out.csv</name>
+        <filemask/>
+        <exclude_filemask/>
+        <file_required>Y</file_required>
+        <include_subfolders>N</include_subfolders>
+      </file>
+    </files>
+    <accept_filenames>N</accept_filenames>
+    <accept_transform_name/>
+    <passing_through_fields>N</passing_through_fields>
+    <accept_field/>
+    <add_to_result_filenames>N</add_to_result_filenames>
+    <error_ignored>N</error_ignored>
+    <file_error_field/>
+    <file_error_message_field/>
+    <skip_bad_files>N</skip_bad_files>
+    <bad_line_files_destination_directory/>
+    <bad_line_files_extension>warning</bad_line_files_extension>
+    <error_line_files_destination_directory/>
+    <error_line_files_extension>error</error_line_files_extension>
+    <line_number_files_destination_directory/>
+    <line_number_files_extension>line</line_number_files_extension>
+    <fields>
+      <field>
+        <name>id</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>one_zero</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>y_n</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>true_false</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>yes_no</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>ja_nee</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>no_format</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>96</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>Dummy</type>
+    <name>OUTPUT</name>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>96</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <order>
+    <hop>
+      <from>Read written text</from>
+      <to>OUTPUT</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
+  <attributes/>
+  <transform_error_handling/>
+</pipeline>
diff --git 
a/integration-tests/transforms/0118-boolean-format-text-file-output.hpl 
b/integration-tests/transforms/0118-boolean-format-text-file-output.hpl
new file mode 100644
index 0000000000..1e36ffce6e
--- /dev/null
+++ b/integration-tests/transforms/0118-boolean-format-text-file-output.hpl
@@ -0,0 +1,417 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <pipeline_version/>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters/>
+    <name>0118-boolean-format-text-file-output</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Issue #5958: reads Booleans written as 1/0, Y/N, true/false 
and yes/no with a Boolean format on each Text File Input field, converts a 
Ja/Nee String to Boolean in Select Values, then writes every field in a 
different Boolean format with Text File Output. The no_format field has neither 
a format nor a length, so it keeps the Y/N default. 
0118-boolean-format-read-back verifies the written text.</description>
+    <extended_description/>
+    <created_user>-</created_user>
+    <modified_user>-</modified_user>
+    <created_date>2026/09/23 12:00:00.000</created_date>
+    <modified_date>2026/09/23 12:00:00.000</modified_date>
+  </info>
+  <transform>
+    <type>TextFileInput2</type>
+    <name>Read Booleans</name>
+    <file_type>CSV</file_type>
+    <compression>None</compression>
+    <separator>;</separator>
+    <enclosure>"</enclosure>
+    <enclosure_breaks>N</enclosure_breaks>
+    <escapechar/>
+    <header>Y</header>
+    <prependFileName>N</prependFileName>
+    <nr_headerlines>1</nr_headerlines>
+    <footer>N</footer>
+    <nr_footerlines>1</nr_footerlines>
+    <line_wrapped>N</line_wrapped>
+    <nr_wraps>1</nr_wraps>
+    <layout_paged>N</layout_paged>
+    <nr_lines_per_page>80</nr_lines_per_page>
+    <nr_lines_doc_header>0</nr_lines_doc_header>
+    <noempty>Y</noempty>
+    <include>N</include>
+    <include_field/>
+    <rownum>N</rownum>
+    <rownum_field/>
+    <rownumByFile>N</rownumByFile>
+    <format>mixed</format>
+    <encoding>UTF-8</encoding>
+    <limit>0</limit>
+    <date_format_lenient>Y</date_format_lenient>
+    <date_format_locale>en_US</date_format_locale>
+    <length>Characters</length>
+    <filters/>
+    <error_count_field/>
+    <error_fields_field/>
+    <error_text_field/>
+    <error_line_skipped>N</error_line_skipped>
+    <schema_definition/>
+    <ignore_fields>N</ignore_fields>
+    <shortFileFieldName/>
+    <extensionFieldName/>
+    <pathFieldName/>
+    <sizeFieldName/>
+    <hiddenFieldName/>
+    <lastModificationTimeFieldName/>
+    <uriNameFieldName/>
+    <rootUriNameFieldName/>
+    <files>
+      <file>
+        <name>${PROJECT_HOME}/files/boolean-formats.csv</name>
+        <filemask/>
+        <exclude_filemask/>
+        <file_required>Y</file_required>
+        <include_subfolders>N</include_subfolders>
+      </file>
+    </files>
+    <accept_filenames>N</accept_filenames>
+    <accept_transform_name/>
+    <passing_through_fields>N</passing_through_fields>
+    <accept_field/>
+    <add_to_result_filenames>N</add_to_result_filenames>
+    <error_ignored>N</error_ignored>
+    <file_error_field/>
+    <file_error_message_field/>
+    <skip_bad_files>N</skip_bad_files>
+    <bad_line_files_destination_directory/>
+    <bad_line_files_extension>warning</bad_line_files_extension>
+    <error_line_files_destination_directory/>
+    <error_line_files_extension>error</error_line_files_extension>
+    <line_number_files_destination_directory/>
+    <line_number_files_extension>line</line_number_files_extension>
+    <fields>
+      <field>
+        <name>id</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Integer</type>
+        <ignore>N</ignore>
+        <format>#</format>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>one_zero</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Boolean</type>
+        <ignore>N</ignore>
+        <format>1/0</format>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>y_n</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Boolean</type>
+        <ignore>N</ignore>
+        <format>Y/N</format>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>true_false</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Boolean</type>
+        <ignore>N</ignore>
+        <format>true/false</format>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>yes_no</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Boolean</type>
+        <ignore>N</ignore>
+        <format>yes/no</format>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>ja_nee</name>
+        <position>-1</position>
+        <length>3</length>
+        <type>String</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+      <field>
+        <name>no_format</name>
+        <position>-1</position>
+        <length>-1</length>
+        <type>Boolean</type>
+        <ignore>N</ignore>
+        <format/>
+        <trim_type>none</trim_type>
+        <precision>-1</precision>
+        <currency/>
+        <decimal/>
+        <group/>
+        <repeat>N</repeat>
+        <null_string/>
+        <if_null_value/>
+      </field>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>96</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>SelectValues</type>
+    <name>ja_nee to Boolean</name>
+    <fields>
+      <select_unspecified>N</select_unspecified>
+      <meta>
+        <name>ja_nee</name>
+        <rename>ja_nee</rename>
+        <type>Boolean</type>
+        <length>-2</length>
+        <precision>-2</precision>
+        <storage_type/>
+        <conversion_mask>Ja/Nee</conversion_mask>
+        <date_format_lenient>N</date_format_lenient>
+        <date_format_locale/>
+        <date_format_timezone/>
+        <lenient_string_to_number>N</lenient_string_to_number>
+        <decimal_symbol/>
+        <grouping_symbol/>
+        <currency_symbol/>
+        <encoding/>
+      </meta>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>320</xloc>
+      <yloc>96</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>TextFileOutput</type>
+    <name>Write other formats</name>
+    <separator>;</separator>
+    <enclosure>"</enclosure>
+    <enclosure_forced>N</enclosure_forced>
+    <enclosure_fix_disabled>N</enclosure_fix_disabled>
+    <header>Y</header>
+    <footer>N</footer>
+    <format>UNIX</format>
+    <compression>None</compression>
+    <encoding>UTF-8</encoding>
+    <endedLine/>
+    <fileNameInField>N</fileNameInField>
+    <fileNameField/>
+    <create_parent_folder>Y</create_parent_folder>
+    <file>
+      <name>${java.io.tmpdir}/0118/boolean-formats-out</name>
+      <do_not_open_new_file_init>N</do_not_open_new_file_init>
+      <extension>csv</extension>
+      <append>N</append>
+      <split>N</split>
+      <haspartno>N</haspartno>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <SpecifyFormat>N</SpecifyFormat>
+      <date_time_format/>
+      <add_to_result_filenames>N</add_to_result_filenames>
+      <pad>N</pad>
+      <fast_dump>N</fast_dump>
+      <splitevery>0</splitevery>
+    </file>
+    <fields>
+      <field>
+        <name>id</name>
+        <type>Integer</type>
+        <format>#</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>one_zero</name>
+        <type>Boolean</type>
+        <format>true/false</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>y_n</name>
+        <type>Boolean</type>
+        <format>1/0</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>true_false</name>
+        <type>Boolean</type>
+        <format>Y/N</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>yes_no</name>
+        <type>Boolean</type>
+        <format>Ja/Nee</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>ja_nee</name>
+        <type>Boolean</type>
+        <format>yes/no</format>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+      <field>
+        <name>no_format</name>
+        <type>Boolean</type>
+        <format/>
+        <currency/>
+        <decimal/>
+        <group/>
+        <nullif/>
+        <trim_type>none</trim_type>
+        <length>-1</length>
+        <precision>-1</precision>
+      </field>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>512</xloc>
+      <yloc>96</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <order>
+    <hop>
+      <from>Read Booleans</from>
+      <to>ja_nee to Boolean</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>ja_nee to Boolean</from>
+      <to>Write other formats</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
+  <attributes/>
+  <transform_error_handling/>
+</pipeline>
diff --git a/integration-tests/transforms/datasets/golden-boolean-formats.csv 
b/integration-tests/transforms/datasets/golden-boolean-formats.csv
new file mode 100644
index 0000000000..dd5a06b4f2
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-boolean-formats.csv
@@ -0,0 +1,5 @@
+id,one_zero,y_n,true_false,yes_no,ja_nee,no_format
+1,true,1,Y,Ja,yes,Y
+2,false,0,N,Nee,no,N
+3,true,0,Y,Nee,yes,Y
+4,,,,,,
diff --git a/integration-tests/transforms/files/boolean-formats.csv 
b/integration-tests/transforms/files/boolean-formats.csv
new file mode 100644
index 0000000000..09d2f461ca
--- /dev/null
+++ b/integration-tests/transforms/files/boolean-formats.csv
@@ -0,0 +1,5 @@
+id;one_zero;y_n;true_false;yes_no;ja_nee;no_format
+1;1;Y;true;yes;Ja;Y
+2;0;N;false;no;Nee;N
+3;1;n;TRUE;No;ja;true
+4;;;;;;
diff --git a/integration-tests/transforms/main-0118-boolean-format.hwf 
b/integration-tests/transforms/main-0118-boolean-format.hwf
new file mode 100644
index 0000000000..f808d9ffb7
--- /dev/null
+++ b/integration-tests/transforms/main-0118-boolean-format.hwf
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0118-boolean-format</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/09/23 12:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/09/23 12:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>START</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <hour>12</hour>
+      <minutes>0</minutes>
+      <weekDay>1</weekDay>
+      <DayOfMonth>1</DayOfMonth>
+      <parallel>N</parallel>
+      <xloc>96</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0118-boolean-format-text-file-output</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      
<filename>${PROJECT_HOME}/0118-boolean-format-text-file-output.hpl</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <set_append_logfile>N</set_append_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <follow_abort_remote>N</follow_abort_remote>
+      <create_parent_folder>N</create_parent_folder>
+      <run_configuration>local</run_configuration>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <parallel>N</parallel>
+      <xloc>288</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0118-boolean-format-read-back UNIT</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0118-boolean-format-read-back UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>544</xloc>
+      <yloc>80</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>START</from>
+      <to>0118-boolean-format-text-file-output</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>0118-boolean-format-text-file-output</from>
+      <to>0118-boolean-format-read-back UNIT</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json 
b/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json
new file mode 100644
index 0000000000..320f54bb12
--- /dev/null
+++ b/integration-tests/transforms/metadata/dataset/golden-boolean-formats.json
@@ -0,0 +1,63 @@
+{
+  "base_filename": "golden-boolean-formats.csv",
+  "name": "golden-boolean-formats",
+  "description": "Text written by Text File Output after re-formatting 
Booleans read in other formats (#5958).",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "id",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "one_zero",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "y_n",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "true_false",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "yes_no",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "ja_nee",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "no_format",
+      "field_format": ""
+    }
+  ]
+}
diff --git 
a/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back 
UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back 
UNIT.json
new file mode 100644
index 0000000000..4e790340dc
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0118-boolean-format-read-back 
UNIT.json   
@@ -0,0 +1,52 @@
+{
+  "variableValues": [],
+  "database_replacements": [],
+  "autoOpening": false,
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "id",
+          "data_set_field": "id"
+        },
+        {
+          "transform_field": "one_zero",
+          "data_set_field": "one_zero"
+        },
+        {
+          "transform_field": "y_n",
+          "data_set_field": "y_n"
+        },
+        {
+          "transform_field": "true_false",
+          "data_set_field": "true_false"
+        },
+        {
+          "transform_field": "yes_no",
+          "data_set_field": "yes_no"
+        },
+        {
+          "transform_field": "ja_nee",
+          "data_set_field": "ja_nee"
+        },
+        {
+          "transform_field": "no_format",
+          "data_set_field": "no_format"
+        }
+      ],
+      "field_order": [
+        "id"
+      ],
+      "transform_name": "OUTPUT",
+      "data_set_name": "golden-boolean-formats"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0118-boolean-format-read-back UNIT",
+  "description": "Reads back the file written by 
0118-boolean-format-text-file-output as text and compares it to the expected 
Boolean formats",
+  "trans_test_tweaks": [],
+  "persist_filename": "",
+  "pipeline_filename": "./0118-boolean-format-read-back.hpl",
+  "test_type": "UNIT_TEST"
+}
diff --git 
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
 
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
index 70190a90e8..979bcd563b 100644
--- 
a/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
+++ 
b/plugins/transforms/concatfields/src/main/java/org/apache/hop/pipeline/transforms/concatfields/ConcatFieldsDialog.java
@@ -284,10 +284,12 @@ public class ConcatFieldsDialog extends 
BaseTransformDialog {
     // Prepare a list of possible formats...
     String[] dats = Const.getDateFormats();
     String[] nums = Const.getNumberFormats();
-    int totalSize = dats.length + nums.length;
+    String[] bools = Const.getBooleanFormats();
+    int totalSize = dats.length + nums.length + bools.length;
     String[] formats = new String[totalSize];
     System.arraycopy(dats, 0, formats, 0, dats.length);
     System.arraycopy(nums, 0, formats, dats.length, nums.length);
+    System.arraycopy(bools, 0, formats, dats.length + nums.length, 
bools.length);
 
     fieldColumns = new ColumnInfo[FieldsCols];
     fieldColumns[0] =
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
index ae58de38fa..768b880e5b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterValueDialog.java
@@ -311,6 +311,15 @@ public class EnterValueDialog extends Dialog {
       case IValueMeta.TYPE_BIGNUMBER:
         wFormat.setItems(new String[] {});
         break;
+      case IValueMeta.TYPE_BOOLEAN:
+        wFormat.setItems(Const.getBooleanFormats());
+        if (!Utils.isEmpty(formatString)) {
+          if (wFormat.indexOf(formatString) < 0) {
+            wFormat.add(formatString);
+          }
+          wFormat.select(wFormat.indexOf(formatString));
+        }
+        break;
       default:
         wFormat.setItems(new String[] {});
         break;
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
index 0b0e127eb5..bccae01587 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TableView.java
@@ -3574,6 +3574,7 @@ public class TableView extends Composite {
         case IValueMeta.TYPE_INTEGER, IValueMeta.TYPE_BIGNUMBER, 
IValueMeta.TYPE_NUMBER ->
             Const.getNumberFormats();
         case IValueMeta.TYPE_STRING -> Const.getConversionFormats();
+        case IValueMeta.TYPE_BOOLEAN -> Const.getBooleanFormats();
         default -> new String[0];
       };
     }

Reply via email to