You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2022/12/26 22:55:04 UTC

[freemarker] 02/02: ?c now works on strings, and it outputs a quoted string literal that's compatible with JSON, and JavaScript. Added ?cn to format missing/null values. Added cFormat setting to specify in what language should ?c output the values.

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

ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git

commit 782daab2958f13181bee5bbc5d069787d0c2c9d7
Author: ddekany <dd...@apache.org>
AuthorDate: Wed Dec 21 21:12:27 2022 +0100

    ?c now works on strings, and it outputs a quoted string literal that's compatible with JSON, and JavaScript. Added ?cn to format missing/null values. Added cFormat setting to specify in what language should ?c output the values.
---
 .../freemarker/core/AbstractJSONLikeFormat.java    |  76 ++
 .../freemarker/core/AbstractLegacyCFormat.java     | 105 +++
 src/main/java/freemarker/core/BuiltIn.java         |   3 +-
 .../freemarker/core/BuiltInsForMultipleTypes.java  | 139 +---
 src/main/java/freemarker/core/CFormat.java         |  80 ++
 .../freemarker/core/CTemplateNumberFormat.java     |  44 +-
 src/main/java/freemarker/core/Configurable.java    | 203 +++---
 .../java/freemarker/core/Default230CFormat.java    |  73 ++
 .../java/freemarker/core/Default2321CFormat.java   |  72 ++
 src/main/java/freemarker/core/Environment.java     | 212 ++++--
 src/main/java/freemarker/core/EvalUtil.java        |   2 +-
 src/main/java/freemarker/core/JSONCFormat.java     |  56 ++
 src/main/java/freemarker/core/JavaCFormat.java     |  87 +++
 .../java/freemarker/core/JavaScriptCFormat.java    |  51 ++
 .../freemarker/core/JavaTemplateNumberFormat.java  |   2 +-
 src/main/java/freemarker/core/PropertySetting.java |  32 +-
 .../freemarker/core/TemplateConfiguration.java     |   9 +-
 src/main/java/freemarker/core/XSCFormat.java       |  91 +++
 .../java/freemarker/core/_StandardCLanguages.java  |  40 +
 .../java/freemarker/template/Configuration.java    |  57 +-
 .../java/freemarker/template/_TemplateAPI.java     |   7 +-
 .../freemarker/template/utility/StringUtil.java    | 166 +++--
 src/manual/en_US/book.xml                          | 812 ++++++++++++++++++---
 .../core/BooleanFormatEnvironmentCachingTest.java  |  57 ++
 .../java/freemarker/core/CAndCnBuiltInTest.java    | 142 ++++
 .../java/freemarker/core/CFormatTemplateTest.java  |  73 ++
 .../freemarker/core/CTemplateNumberFormatTest.java |  11 +-
 src/test/java/freemarker/core/CustomCFormat.java   |  83 +++
 .../java/freemarker/core/NumberFormatTest.java     |   7 +-
 .../freemarker/core/TemplateConfigurationTest.java |   1 +
 .../freemarker/template/ConfigurationTest.java     |  58 +-
 .../template/utility/StringUtilTest.java           |   2 +-
 .../test/templatesuite/templates/number-format.ftl |  42 +-
 33 files changed, 2409 insertions(+), 486 deletions(-)

diff --git a/src/main/java/freemarker/core/AbstractJSONLikeFormat.java b/src/main/java/freemarker/core/AbstractJSONLikeFormat.java
new file mode 100644
index 00000000..190ce697
--- /dev/null
+++ b/src/main/java/freemarker/core/AbstractJSONLikeFormat.java
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+/**
+ * Defines the methods in {@link CFormat} that are the same for all JSON-like languages.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public abstract class AbstractJSONLikeFormat extends CFormat {
+    private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+            "Infinity", "-Infinity", "NaN",
+            "Infinity", "-Infinity", "NaN");
+
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+    static {
+        DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+        symbols.setInfinity("Infinity");
+        symbols.setNaN("NaN");
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+    }
+
+    // Visibility is not "protected" to avoid external implementations while this class is experimental.
+    AbstractJSONLikeFormat() {
+    }
+
+    @Override
+    String getTrueString() {
+        return "true";
+    }
+
+    @Override
+    String getFalseString() {
+        return "false";
+    }
+
+    @Override
+    final String getNullString() {
+        return "null";
+    }
+
+    @Override
+    final TemplateNumberFormat getTemplateNumberFormat() {
+        return TEMPLATE_NUMBER_FORMAT;
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+}
diff --git a/src/main/java/freemarker/core/AbstractLegacyCFormat.java b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
new file mode 100644
index 00000000..99440684
--- /dev/null
+++ b/src/main/java/freemarker/core/AbstractLegacyCFormat.java
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateNumberModel;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * Super class of {@link CFormat}-s that merely exist to mimic old {@code ?c} behavior for backward compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ * @see AbstractJSONLikeFormat
+ */
+public abstract class AbstractLegacyCFormat extends CFormat {
+    // Visibility is not "protected" to avoid external implementations while this class is experimental.
+    AbstractLegacyCFormat() {
+    }
+
+    @Override
+    final String formatString(String s, Environment env) throws TemplateException {
+        return StringUtil.jsStringEnc(s, true, true);
+    }
+
+    @Override
+    final TemplateNumberFormat getTemplateNumberFormat() {
+        return new LegacyCTemplateNumberFormat();
+    }
+
+    @Override
+    String getTrueString() {
+        return "true";
+    }
+
+    @Override
+    String getFalseString() {
+        return "false";
+    }
+
+    @Override
+    final String getNullString() {
+        return "null";
+    }
+
+    abstract NumberFormat getLegacyNumberFormat();
+
+    final class LegacyCTemplateNumberFormat extends JavaTemplateNumberFormat {
+
+        public LegacyCTemplateNumberFormat() {
+            super(getLegacyNumberFormat(), Environment.COMPUTER_FORMAT_STRING);
+        }
+
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel) throws UnformattableValueException,
+                TemplateModelException {
+            Number number = TemplateFormatUtil.getNonNullNumber(numberModel);
+            return format(number);
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return false;
+        }
+
+        @Override
+        String format(Number number) throws UnformattableValueException {
+            if (number instanceof Integer || number instanceof Long) {
+                // Accelerate these fairly common cases
+                return number.toString();
+            }
+            return super.format(number);
+        }
+
+        @Override
+        public String getDescription() {
+            return "LegacyC(" + super.getDescription() + ")";
+        }
+
+    }
+
+}
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 1631b76b..708c562e 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -85,7 +85,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
 
     static final Set<String> CAMEL_CASE_NAMES = new TreeSet<>();
     static final Set<String> SNAKE_CASE_NAMES = new TreeSet<>();
-    static final int NUMBER_OF_BIS = 295;
+    static final int NUMBER_OF_BIS = 296;
     static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new HashMap<>(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
 
     static final String BI_NAME_SNAKE_CASE_WITH_ARGS = "with_args";
@@ -103,6 +103,7 @@ abstract class BuiltIn extends Expression implements Cloneable {
         putBI("boolean", new BuiltInsForStringsMisc.booleanBI());
         putBI("byte", new byteBI());
         putBI("c", new BuiltInsForMultipleTypes.cBI());
+        putBI("cn", new BuiltInsForMultipleTypes.cnBI());
         putBI("cap_first", "capFirst", new BuiltInsForStringsBasic.cap_firstBI());
         putBI("capitalize", new BuiltInsForStringsBasic.capitalizeBI());
         putBI("ceiling", new ceilingBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index b214d8c3..e2eb2731 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -56,95 +56,52 @@ import freemarker.template.utility.NumberUtil;
  */
 class BuiltInsForMultipleTypes {
 
-    static class cBI extends AbstractCBI implements ICIChainMember {
-        
-        static class BIBeforeICI2d3d21 extends AbstractCBI {
-
-            @Override
-            protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
-                Number num = EvalUtil.modelToNumber((TemplateNumberModel) model, target);
-                if (num instanceof Integer || num instanceof Long) {
-                    // Accelerate these fairly common cases
-                    return new SimpleScalar(num.toString());
-                } else {
-                    return new SimpleScalar(env.getCNumberFormat().format(num));
-                }
-            }
-            
+    static class cBI extends AbstractCLikeBI {
+        final protected String formatNull(Environment env) throws InvalidReferenceException {
+            throw InvalidReferenceException.getInstance(target, env);
         }
+    }
 
-        static class BIBeforeICI2d3d32 extends AbstractCBI implements ICIChainMember {
-            private final BIBeforeICI2d3d21 prevICIObj = new BIBeforeICI2d3d21();
-
-            @Override
-            protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateModelException {
-                Number num = EvalUtil.modelToNumber((TemplateNumberModel) model, target);
-                if (num instanceof Integer || num instanceof Long) {
-                    // Accelerate these fairly common cases
-                    return new SimpleScalar(num.toString());
-                // INF etc. is properly handled by ?c since 2.3.21, but the getCNumberFormat returns the pre 2.3.21
-                // format before IcI 2.3.31, so we keep these here:
-                } else if (num instanceof Double) {
-                    double n = num.doubleValue();
-                    if (n == Double.POSITIVE_INFINITY) {
-                        return new SimpleScalar("INF");
-                    }
-                    if (n == Double.NEGATIVE_INFINITY) {
-                        return new SimpleScalar("-INF");
-                    }
-                    if (Double.isNaN(n)) {
-                        return new SimpleScalar("NaN");
-                    }
-                    // Deliberately falls through
-                } else if (num instanceof Float) {
-                    float n = num.floatValue();
-                    if (n == Float.POSITIVE_INFINITY) {
-                        return new SimpleScalar("INF");
-                    }
-                    if (n == Float.NEGATIVE_INFINITY) {
-                        return new SimpleScalar("-INF");
-                    }
-                    if (Float.isNaN(n)) {
-                        return new SimpleScalar("NaN");
-                    }
-                    // Deliberately falls through
-                }
-
-                return new SimpleScalar(env.getCNumberFormat().format(num));
-            }
-
-            @Override
-            public int getMinimumICIVersion() {
-                return _VersionInts.V_2_3_21;
-            }
-
-            @Override
-            public Object getPreviousICIChainMember() {
-                return prevICIObj;
-            }
+    static class cnBI extends AbstractCLikeBI {
+        final protected String formatNull(Environment env) {
+            return env.getCFormat().getNullString();
         }
+    }
+
+    private static abstract class AbstractCLikeBI extends BuiltIn {
 
         @Override
-        protected TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateException {
-            try {
-                return new SimpleScalar(CTemplateNumberFormat.INSTANCE.formatToPlainText((TemplateNumberModel) model));
-            } catch (TemplateValueFormatException e) {
-                throw _MessageUtil.newCantFormatNumberException(CTemplateNumberFormat.INSTANCE, target, e, false);
+        final TemplateModel _eval(Environment env) throws TemplateException {
+            final String result;
+            final TemplateModel model = target.eval(env);
+            if (model instanceof TemplateNumberModel) {
+                TemplateNumberFormat cTemplateNumberFormat = env.getCTemplateNumberFormat();
+                try {
+                    result = cTemplateNumberFormat.formatToPlainText((TemplateNumberModel) model);
+                } catch (TemplateValueFormatException e) {
+                    throw _MessageUtil.newCantFormatNumberException(cTemplateNumberFormat, target, e, false);
+                }
+            } else if (model instanceof TemplateBooleanModel) {
+                boolean b = ((TemplateBooleanModel) model).getAsBoolean();
+                CFormat cFormat = env.getCFormat();
+                result = b ? cFormat.getTrueString() : cFormat.getFalseString();
+            } else if (model instanceof TemplateScalarModel) {
+                String s = EvalUtil.modelToString((TemplateScalarModel) model, target, env);
+                result = env.getCFormat().formatString(s, env);
+            } else if (model == null) {
+                result = formatNull(env);
+            } else {
+                throw new UnexpectedTypeException(
+                        target, model,
+                        "number, boolean, or string",
+                        new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class, TemplateScalarModel.class },
+                        env);
             }
+            return new SimpleScalar(result);
         }
 
-        private final BIBeforeICI2d3d32 prevICIObj = new BIBeforeICI2d3d32();
+        protected abstract String formatNull(Environment env) throws InvalidReferenceException;
 
-        @Override
-        public int getMinimumICIVersion() {
-            return _VersionInts.V_2_3_32;
-        }
-        
-        @Override
-        public Object getPreviousICIChainMember() {
-            return prevICIObj;
-        }
-        
     }
 
     static class dateBI extends BuiltIn {
@@ -802,26 +759,4 @@ class BuiltInsForMultipleTypes {
     // Can't be instantiated
     private BuiltInsForMultipleTypes() { }
 
-    static abstract class AbstractCBI extends BuiltIn {
-        
-        @Override
-        final TemplateModel _eval(Environment env) throws TemplateException {
-            TemplateModel model = target.eval(env);
-            if (model instanceof TemplateNumberModel) {
-                return formatNumber(env, model);
-            } else if (model instanceof TemplateBooleanModel) {
-                return new SimpleScalar(((TemplateBooleanModel) model).getAsBoolean()
-                        ? MiscUtil.C_TRUE : MiscUtil.C_FALSE);
-            } else {
-                throw new UnexpectedTypeException(
-                        target, model,
-                        "number or boolean", new Class[] { TemplateNumberModel.class, TemplateBooleanModel.class },
-                        env);
-            }
-        }
-
-        protected abstract TemplateModel formatNumber(Environment env, TemplateModel model) throws TemplateException;
-        
-    }
-
 }
diff --git a/src/main/java/freemarker/core/CFormat.java b/src/main/java/freemarker/core/CFormat.java
new file mode 100644
index 00000000..3c04976c
--- /dev/null
+++ b/src/main/java/freemarker/core/CFormat.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+
+/**
+ * Defines a format (usually a computer language) that's used by the {@code c}, {@code cn} built-ins, and for the
+ * {@code "c"} and {@code "computer"} number formats (see {@link Configurable#setNumberFormat(String)}). A
+ * {@link CFormat} currently defines how numbers, booleans, and strings are converted to text that defines a similar
+ * value in the computer language that the {@link CFormat} is made for.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public abstract class CFormat {
+
+    // Visibility is not "protected" to avoid external implementations while this class is experimental.
+    CFormat() {
+    }
+
+    /**
+     * Gets/creates the number format to use; this is not always a cheap operation, so in case it will be called
+     * for many cases, the result should be cached or otherwise reused.
+     *
+     * <p>The returned object is typically not thread-safe. The implementation must ensure that if there's a singleton,
+     * which is mutable, or not thread-safe, then it's not returned, but a clone or copy of it. The caller of this
+     * method is not responsible for do any such cloning or copying.
+     */
+    abstract TemplateNumberFormat getTemplateNumberFormat();
+
+    /**
+     * Similar to {@link #getTemplateNumberFormat()}, but only exist to serve the deprecated
+     * {@link Environment#getCNumberFormat()} method. We don't expect the result of the formatting to be the same as
+     * with the {@link TemplateNumberFormat}, but this method should make some effort to be similar.
+     *
+     * @deprecated Use {@link #getTemplateNumberFormat()} instead, except in {@link Environment#getCNumberFormat()}.
+     */
+    @Deprecated
+    abstract NumberFormat getLegacyNumberFormat();
+
+    /**
+     * Format a {@link String} to a string literal.
+     *
+     * @param env
+     *      Not {@code null}; is here mostly to be used to figure out escaping preferences (like based on
+     *      {@link Environment#getOutputEncoding()}).
+     */
+    abstract String formatString(String s, Environment env) throws TemplateException;
+
+    abstract String getTrueString();
+
+    abstract String getFalseString();
+
+    abstract String getNullString();
+
+    public abstract String getName();
+}
diff --git a/src/main/java/freemarker/core/CTemplateNumberFormat.java b/src/main/java/freemarker/core/CTemplateNumberFormat.java
index 139e2d72..dc662b7a 100644
--- a/src/main/java/freemarker/core/CTemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/CTemplateNumberFormat.java
@@ -19,23 +19,44 @@
 
 package freemarker.core;
 
-
 import java.math.BigDecimal;
 import java.math.BigInteger;
 
+import freemarker.template.Configuration;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 
 /**
+ * The number format used by most {@link CFormat}-s starting from Incompatible Improvements
+ * {@link Configuration#VERSION_2_3_32}.
+ *
+ * <p>This {@link TemplateNumberFormat} implementation is thread-safe an immutable.
+ *
+ * <p>This is a lossless format, except that the number types are not kept. That is, the original number can always be
+ * exactly restored from the string representation, but only if you know what the type of the number was.
+ *
  * @since 2.3.32
  */
 final class CTemplateNumberFormat extends TemplateNumberFormat {
-    static final TemplateNumberFormat INSTANCE = new CTemplateNumberFormat();
-
     private static final float MAX_INCREMENT_1_FLOAT = 16777216f;
     private static final double MAX_INCREMENT_1_DOUBLE = 9007199254740992d;
 
-    private CTemplateNumberFormat() {
+    private final String doublePositiveInfinity;
+    private final String doubleNegativeInfinity;
+    private final String doubleNaN;
+    private final String floatPositiveInfinity;
+    private final String floatNegativeInfinity;
+    private final String floatNaN;
+
+    CTemplateNumberFormat(
+            String doublePositiveInfinity, String doubleNegativeInfinity, String doubleNaN,
+            String floatPositiveInfinity, String floatNegativeInfinity, String floatNaN) {
+        this.doublePositiveInfinity = doublePositiveInfinity;
+        this.doubleNegativeInfinity = doubleNegativeInfinity;
+        this.doubleNaN = doubleNaN;
+        this.floatPositiveInfinity = floatPositiveInfinity;
+        this.floatNegativeInfinity = floatNegativeInfinity;
+        this.floatNaN = floatNaN;
     }
 
     @Override
@@ -49,13 +70,13 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
         } else if (num instanceof Double) {
             double n = num.doubleValue();
             if (n == Double.POSITIVE_INFINITY) {
-                return "INF";
+                return doublePositiveInfinity;
             }
             if (n == Double.NEGATIVE_INFINITY) {
-                return "-INF";
+                return doubleNegativeInfinity;
             }
             if (Double.isNaN(n)) {
-                return "NaN";
+                return doubleNaN;
             }
             if (Math.floor(n) == n) {
                 if (Math.abs(n) <= MAX_INCREMENT_1_DOUBLE) {
@@ -84,13 +105,13 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
             float n = num.floatValue();
 
             if (n == Float.POSITIVE_INFINITY) {
-                return "INF";
+                return floatPositiveInfinity;
             }
             if (n == Float.NEGATIVE_INFINITY) {
-                return "-INF";
+                return floatNegativeInfinity;
             }
             if (Float.isNaN(n)) {
-                return "NaN";
+                return floatNaN;
             }
             if (Math.floor(n) == n) {
                 if (Math.abs(n) <= MAX_INCREMENT_1_FLOAT) {
@@ -117,7 +138,8 @@ final class CTemplateNumberFormat extends TemplateNumberFormat {
             if (scale <= 0) {
                 // A whole number. Myabe a long ID in a database or other system, and for those exponential form is not
                 // expected generally, so we avoid that. But then, it becomes too easy to write something like
-                // 1e1000000000000 and kill the server with a terra byte long rendering of the number, so for lengths that
+                // 1e1000000000000 and kill the server with a terra byte long rendering of the number, so for lengths
+                // that
                 // realistically aren't ID-s or such, we use exponential format after all:
                 if (scale <= -100) {
                     return bd.toString(); // Will give exponential form for this scale
diff --git a/src/main/java/freemarker/core/Configurable.java b/src/main/java/freemarker/core/Configurable.java
index dd38163a..428ac41d 100644
--- a/src/main/java/freemarker/core/Configurable.java
+++ b/src/main/java/freemarker/core/Configurable.java
@@ -96,7 +96,14 @@ public class Configurable {
     public static final String LOCALE_KEY_CAMEL_CASE = "locale";
     /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
     public static final String LOCALE_KEY = LOCALE_KEY_SNAKE_CASE;
-    
+
+    /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
+    public static final String C_FORMAT_KEY_SNAKE_CASE = "c_format";
+    /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
+    public static final String C_FORMAT_KEY_CAMEL_CASE = "cFormat";
+    /** Alias to the {@code ..._SNAKE_CASE} variation due to backward compatibility constraints. */
+    public static final String C_FORMAT_KEY = C_FORMAT_KEY_SNAKE_CASE;
+
     /** Legacy, snake case ({@code like_this}) variation of the setting name. @since 2.3.23 */
     public static final String NUMBER_FORMAT_KEY_SNAKE_CASE = "number_format";
     /** Modern, camel case ({@code likeThis}) variation of the setting name. @since 2.3.23 */
@@ -306,6 +313,7 @@ public class Configurable {
         AUTO_IMPORT_KEY_SNAKE_CASE,
         AUTO_INCLUDE_KEY_SNAKE_CASE,
         BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+        C_FORMAT_KEY_SNAKE_CASE,
         CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
         CUSTOM_DATE_FORMATS_KEY_SNAKE_CASE,
         CUSTOM_NUMBER_FORMATS_KEY_SNAKE_CASE,
@@ -339,6 +347,7 @@ public class Configurable {
         AUTO_IMPORT_KEY_CAMEL_CASE,
         AUTO_INCLUDE_KEY_CAMEL_CASE,
         BOOLEAN_FORMAT_KEY_CAMEL_CASE,
+        C_FORMAT_KEY_CAMEL_CASE,
         CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
         CUSTOM_DATE_FORMATS_KEY_CAMEL_CASE,
         CUSTOM_NUMBER_FORMATS_KEY_CAMEL_CASE,
@@ -368,6 +377,7 @@ public class Configurable {
     private HashMap<Object, Object> customAttributes;
     
     private Locale locale;
+    private CFormat cFormat;
     private String numberFormat;
     private String timeFormat;
     private String dateFormat;
@@ -376,8 +386,8 @@ public class Configurable {
     private TimeZone sqlDataAndTimeTimeZone;
     private boolean sqlDataAndTimeTimeZoneSet;
     private String booleanFormat;
-    private String trueStringValue;  // deduced from booleanFormat
-    private String falseStringValue;  // deduced from booleanFormat
+    private String booleanFormatCommaSplitTrueSide;  // deduced from booleanFormat
+    private String booleanFormatCommaSplitFalseSide;  // deduced from booleanFormat
     private Integer classicCompatible;
     private TemplateExceptionHandler templateExceptionHandler;
     private AttemptExceptionReporter attemptExceptionReporter;
@@ -425,7 +435,7 @@ public class Configurable {
         
         locale = _TemplateAPI.getDefaultLocale();
         properties.setProperty(LOCALE_KEY, locale.toString());
-        
+
         timeZone = _TemplateAPI.getDefaultTimeZone();
         properties.setProperty(TIME_ZONE_KEY, timeZone.getID());
         
@@ -443,7 +453,9 @@ public class Configurable {
         
         dateTimeFormat = "";
         properties.setProperty(DATETIME_FORMAT_KEY, dateTimeFormat);
-        
+
+        cFormat = _TemplateAPI.getDefaultCFormat(incompatibleImprovements);
+
         classicCompatible = Integer.valueOf(0);
         properties.setProperty(CLASSIC_COMPATIBLE_KEY, classicCompatible.toString());
         
@@ -678,12 +690,43 @@ public class Configurable {
     /**
      * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
      *  
+     * @since 2.3.32
+     */
+    public boolean isCFormatSet() {
+        return cFormat != null;
+    }
+
+    /**
+     * Sets the computer language that's used for the {@code c}, {@code cn} built-ins, and for the {@code "c"}
+     * (and {@code "computer"}) number format ({@link Environment#getCTemplateNumberFormat()}). That is, of the
+     * templates output pieces in a computer language (like JavaScript), you should set what's that here.
+     *
+     * @since 2.3.32
+     */
+    public void setCFormat(CFormat cFormat) {
+        NullArgumentException.check("cFormat", cFormat);
+        this.cFormat = cFormat;
+    }
+
+    /**
+     * Getter pair of {@link #setCFormat(CFormat)}. Not {@code null}.
+     *
+     * @since 2.3.32
+     */
+    public CFormat getCFormat() {
+        return cFormat != null ? cFormat : parent.getCFormat();
+    }
+
+    /**
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     *
      * @since 2.3.24
      */
     public boolean isLocaleSet() {
         return locale != null;
     }
-    
+
+
     /**
      * Sets the time zone to use when formatting date/time values.
      * Defaults to the system time zone ({@link TimeZone#getDefault()}), regardless of the "locale" FreeMarker setting,
@@ -815,7 +858,7 @@ public class Configurable {
      *   <li>{@code "number"}: The number format returned by {@link NumberFormat#getNumberInstance(Locale)}</li>
      *   <li>{@code "currency"}: The number format returned by {@link NumberFormat#getCurrencyInstance(Locale)}</li>
      *   <li>{@code "percent"}: The number format returned by {@link NumberFormat#getPercentInstance(Locale)}</li>
-     *   <li>{@code "c"} (recognized since 2.3.34), or {@code "computer"} (same as {@code "c"}, but also recognized by
+     *   <li>{@code "c"} (recognized since 2.3.32), or {@code "computer"} (same as {@code "c"}, but also recognized by
      *       older versions): The number format used by FTL's {@code c} built-in (like in {@code someNumber?c}).</li>
      *   <li>{@link java.text.DecimalFormat} pattern (like {@code "0.##"}). This syntax is extended by FreeMarker
      *       so that you can specify options like the rounding mode and the symbols used after a 2nd semicolon. For
@@ -985,14 +1028,9 @@ public class Configurable {
     public void setBooleanFormat(String booleanFormat) {
         NullArgumentException.check("booleanFormat", booleanFormat);
 
-        if (booleanFormat.equals(C_TRUE_FALSE)) {
-            // C_TRUE_FALSE is the default for BC, but it's not a good default for human audience formatting, so we
-            // pretend that it wasn't set.
-            trueStringValue = null; 
-            falseStringValue = null;
-        } else if (booleanFormat.equals(C_FORMAT_STRING)) {
-            trueStringValue = MiscUtil.C_TRUE;
-            falseStringValue = MiscUtil.C_FALSE;
+        if (booleanFormat.equals(C_TRUE_FALSE) || booleanFormat.equals(C_FORMAT_STRING)) {
+            booleanFormatCommaSplitTrueSide = null;
+            booleanFormatCommaSplitFalseSide = null;
         } else {
             int commaIdx = booleanFormat.indexOf(',');
             if (commaIdx == -1) {
@@ -1001,8 +1039,8 @@ public class Configurable {
                                 "or it must be \"" + C_FORMAT_STRING + "\", but it was " +
                                 StringUtil.jQuote(booleanFormat) + ".");
             }
-            trueStringValue = booleanFormat.substring(0, commaIdx);
-            falseStringValue = booleanFormat.substring(commaIdx + 1);
+            booleanFormatCommaSplitTrueSide = booleanFormat.substring(0, commaIdx);
+            booleanFormatCommaSplitFalseSide = booleanFormat.substring(commaIdx + 1);
         }
 
         this.booleanFormat = booleanFormat;
@@ -1015,91 +1053,40 @@ public class Configurable {
     public String getBooleanFormat() {
         return booleanFormat != null ? booleanFormat : parent.getBooleanFormat(); 
     }
-    
+
     /**
-     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
-     *  
-     * @since 2.3.24
+     * Non-{@code null} if the {@link #setBooleanFormat(String)} boolean_format} setting is comma separated true, and
+     * false strings.
+     *
+     * @since 2.3.32
      */
-    public boolean isBooleanFormatSet() {
-        return booleanFormat != null;
-    }
-        
-    String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
-        if (value) {
-            String s = getTrueStringValue();
-            if (s == null) {
-                if (fallbackToTrueFalse) {
-                    return MiscUtil.C_TRUE;
-                } else {
-                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
-                }
-            } else {
-                return s;
-            }
-        } else {
-            String s = getFalseStringValue();
-            if (s == null) {
-                if (fallbackToTrueFalse) {
-                    return MiscUtil.C_FALSE;
-                } else {
-                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
-                }
-            } else {
-                return s;
-            }
+    String getBooleanFormatCommaSplitTrueSide() {
+        if (booleanFormat != null) {
+            return booleanFormatCommaSplitTrueSide;
         }
-    }
-
-    private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
-        return new _ErrorDescriptionBuilder(
-                "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
-                new _DelayedJQuote(getBooleanFormat()), 
-                (getBooleanFormat().equals(C_TRUE_FALSE)
-                    ? ", which is the legacy deprecated default, and we treat it as if no format was set. "
-                            + "This is the default configuration; you should provide the format explicitly for each "
-                            + "place where you print a boolean."
-                    : ".")
-                ).tips(
-                     "Write something like myBool?string('yes', 'no') to specify boolean formatting in place.",
-                    new Object[]{
-                        "If you want \"true\"/\"false\" result as you are generating computer-language output "
-                                + "(not for direct human consumption), then use \"?c\", like ${myBool?c}. (If you "
-                                + "always generate computer-language output, then it's might be reasonable to set "
-                                + "the \"", BOOLEAN_FORMAT_KEY, "\" setting to \"c\" instead.)",
-                    },
-                    new Object[] {
-                        "If you need the same two values on most places, the programmers can set the \"",
-                        BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\". However, then it will be easy to "
-                                + "unwillingly format booleans like that."
-                    }
-                 );
+        return parent != null ? parent.getBooleanFormatCommaSplitTrueSide() : null;
     }
 
     /**
-     * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic
-     * coercion to string is not allowed. The default value is {@code null}.
-     * 
-     * <p>This value is deduced from the {@code "boolean_format"} setting.
-     * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this
-     * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a
-     * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's
-     * ignored.
-     * 
-     * @since 2.3.20
+     * Non-{@code null} if the {@link #setBooleanFormat(String)} boolean_format} setting is comma separated true, and
+     * false strings.
+     *
+     * @since 2.3.32
      */
-    String getTrueStringValue() {
-        // The first step deliberately tests booleanFormat instead of trueStringValue! 
-        return booleanFormat != null ? trueStringValue : (parent != null ? parent.getTrueStringValue() : null); 
+    String getBooleanFormatCommaSplitFalseSide() {
+        if (booleanFormat != null) {
+            return booleanFormatCommaSplitFalseSide;
+        }
+        return parent != null ? parent.getBooleanFormatCommaSplitFalseSide() : null;
     }
 
     /**
-     * Same as {@link #getTrueStringValue()} but with {@code false}. 
-     * @since 2.3.20
+     * Tells if this setting is set directly in this object or its value is coming from the {@link #getParent() parent}.
+     *  
+     * @since 2.3.24
      */
-    String getFalseStringValue() {
-        // The first step deliberately tests booleanFormat instead of falseStringValue! 
-        return booleanFormat != null ? falseStringValue : (parent != null ? parent.getFalseStringValue() : null); 
+    public boolean isBooleanFormatSet() {
+        return booleanFormat != null;
     }
 
     /**
@@ -2178,7 +2165,7 @@ public class Configurable {
      *       See {@link #setLocale(Locale)}.
      *       <br>String value: local codes with the usual format in Java, such as {@code "en_US"}, or since 2.3.26,
      *       "JVM default" (ignoring case) to use the default locale of the Java environment.
-     *       
+     *
      *   <li><p>{@code "classic_compatible"}:
      *       See {@link #setClassicCompatible(boolean)} and {@link Configurable#setClassicCompatibleAsInt(int)}.
      *       <br>String value: {@code "true"}, {@code "false"}, also since 2.3.20 {@code 0} or {@code 1} or {@code 2}.
@@ -2194,7 +2181,15 @@ public class Configurable {
      *   <br>String value: Interpreted as an <a href="#fm_obe">object builder expression</a>.
      *   <br>Example: <code>{ "trade": com.example.TradeTemplateDateFormatFactory,
      *   "log": com.example.LogTemplateDateFormatFactory }</code>
-     *       
+     *
+     *   <li><p>{@code "c_format"}:
+     *       See {@link Configuration#setCFormat(CFormat)}.
+     *       <br>String value: {@code "default"} (case insensitive) for the default (on {@link Configuration} only), or
+     *       one of the predefined values {@value JSONCFormat#NAME}, {@value JavaScriptCFormat#NAME},
+     *       {@value JavaCFormat#NAME}, {@value XSCFormat#NAME}, {@value Default230CFormat#NAME},
+     *       {@value Default2321CFormat#NAME}, or an <a href="#fm_obe">object builder expression</a> that gives a
+     *       {@link CFormat} object.
+     *
      *   <li><p>{@code "template_exception_handler"}:
      *       See {@link #setTemplateExceptionHandler(TemplateExceptionHandler)}.
      *       <br>String value: If the value contains dot, then it's interpreted as an <a href="#fm_obe">object builder
@@ -2249,7 +2244,7 @@ public class Configurable {
      *        
      *   <li><p>{@code "time_zone"}:
      *       See {@link #setTimeZone(TimeZone)}.
-     *       <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, since 2.3.21
+     *       <br>String value: With the format as {@link TimeZone#getTimeZone(String)} defines it. Also, since 2.3.21
      *       {@code "JVM default"} can be used that will be replaced with the actual JVM default time zone when
      *       {@link #setSetting(String, String)} is called.
      *       For example {@code "GMT-8:00"} or {@code "America/Los_Angeles"}
@@ -2259,8 +2254,8 @@ public class Configurable {
      *   <li><p>{@code sql_date_and_time_time_zone}:
      *       See {@link #setSQLDateAndTimeTimeZone(TimeZone)}.
      *       Since 2.3.21.
-     *       <br>String value: With the format as {@link TimeZone#getTimeZone} defines it. Also, {@code "JVM default"}
-     *       can be used that will be replaced with the actual JVM default time zone when
+     *       <br>String value: With the format as {@link TimeZone#getTimeZone(String)} defines it. Also,
+     *       {@code "JVM default"} can be used that will be replaced with the actual JVM default time zone when
      *       {@link #setSetting(String, String)} is called. Also {@code "null"} can be used, which has the same effect
      *       as {@link #setSQLDateAndTimeTimeZone(TimeZone) setSQLDateAndTimeTimeZone(null)}.
      *       
@@ -2410,7 +2405,7 @@ public class Configurable {
      *       <br>String value: {@code "default"} (case insensitive) for the default, or an
      *       <a href="#fm_obe">object builder expression</a> that gives an {@link OutputFormat}, for example
      *       {@code HTMLOutputFormat} or {@code XMLOutputFormat}.
-     *       
+     *
      *   <li><p>{@code "registered_custom_output_formats"}:
      *       See {@link Configuration#setRegisteredCustomOutputFormats(Collection)}.
      *       <br>String value: an <a href="#fm_obe">object builder expression</a> that gives a {@link List} of
@@ -2750,6 +2745,20 @@ public class Configurable {
                 }
             } else if (BOOLEAN_FORMAT_KEY_SNAKE_CASE.equals(name) || BOOLEAN_FORMAT_KEY_CAMEL_CASE.equals(name)) {
                 setBooleanFormat(value);
+            } else if (C_FORMAT_KEY_SNAKE_CASE.equals(name) || C_FORMAT_KEY_CAMEL_CASE.equals(name)) {
+                if (value.equalsIgnoreCase(DEFAULT)) {
+                    if (this instanceof Configuration) {
+                        ((Configuration) this).unsetCFormat();
+                    } else {
+                        throw invalidSettingValueException(name, value);
+                    }
+                } else {
+                    CFormat cFormat = StandardCFormats.STANDARD_C_FORMATS.get(value);
+                    setCFormat(
+                            cFormat != null ? cFormat
+                                    : (CFormat) _ObjectBuilderSettingEvaluator.eval(
+                                            value, CFormat.class, false, _SettingEvaluationEnvironment.getCurrent()));
+                }
             } else if (OUTPUT_ENCODING_KEY_SNAKE_CASE.equals(name) || OUTPUT_ENCODING_KEY_CAMEL_CASE.equals(name)) {
                 setOutputEncoding(value);
             } else if (URL_ESCAPING_CHARSET_KEY_SNAKE_CASE.equals(name)
diff --git a/src/main/java/freemarker/core/Default230CFormat.java b/src/main/java/freemarker/core/Default230CFormat.java
new file mode 100644
index 00000000..b1ba1b4e
--- /dev/null
+++ b/src/main/java/freemarker/core/Default230CFormat.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * Corresponds to the behavior of {@code ?c} if
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is less than
+ * {@link Configuration#VERSION_2_3_21}.
+ * The only good reason for using this is strict backward-compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @see Default2321CFormat
+ * @see JSONCFormat
+ *
+ * @since 2.3.32
+ */
+public class Default230CFormat extends AbstractLegacyCFormat {
+    public static final Default230CFormat INSTANCE = new Default230CFormat();
+    public static final String NAME = "default 2.3.0";
+
+    /**
+     * "c" number format as it was before Incompatible Improvements 2.3.21.
+     */
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = new DecimalFormat(
+            "0.################",
+            new DecimalFormatSymbols(Locale.US));
+    static {
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setGroupingUsed(false);
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalSeparatorAlwaysShown(false);
+    }
+
+    private Default230CFormat() {
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/Default2321CFormat.java b/src/main/java/freemarker/core/Default2321CFormat.java
new file mode 100644
index 00000000..d9a6463a
--- /dev/null
+++ b/src/main/java/freemarker/core/Default2321CFormat.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.Configuration;
+import freemarker.template.Version;
+
+/**
+ * Corresponds to the behavior of {@code ?c} if
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is between
+ * {@link Configuration#VERSION_2_3_21} and {@link Configuration#VERSION_2_3_31}.
+ * The only good reason for using this is strict backward-compatibility.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * constructor and most methods are not exposed outside FreeMarker, and so you can't create a custom implementation.
+ * The class itself and some members are exposed as they are needed for configuring FreeMarker.
+ *
+ * @see Default230CFormat
+ * @see JSONCFormat
+ *
+ * @since 2.3.32
+ */
+public class Default2321CFormat extends AbstractLegacyCFormat {
+    public static final Default2321CFormat INSTANCE = new Default2321CFormat();
+    public static final String NAME = "default 2.3.21";
+
+    /**
+     * "c" number format as it was starting from Incompatible Improvements 2.3.21.
+     */
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+    static {
+        DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+        symbols.setInfinity("INF");
+        symbols.setNaN("NaN");
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+    }
+
+    private Default2321CFormat() {
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index a078eaea..6a00a788 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -26,8 +26,6 @@ import java.io.Writer;
 import java.sql.Time;
 import java.sql.Timestamp;
 import java.text.Collator;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -104,28 +102,6 @@ public final class Environment extends Configurable {
     private static final Logger LOG = Logger.getLogger("freemarker.runtime");
     private static final Logger ATTEMPT_LOGGER = Logger.getLogger("freemarker.runtime.attempt");
 
-    // Do not use this object directly; clone it first! DecimalFormat isn't
-    // thread-safe.
-    /** "c" number format as it was before Incompatible Improvements 2.3.21. */
-    private static final DecimalFormat C_NUMBER_FORMAT_ICI_2_3_20 = new DecimalFormat(
-            "0.################",
-            new DecimalFormatSymbols(Locale.US));
-    static {
-        C_NUMBER_FORMAT_ICI_2_3_20.setGroupingUsed(false);
-        C_NUMBER_FORMAT_ICI_2_3_20.setDecimalSeparatorAlwaysShown(false);
-    }
-
-    // Do not use this object directly; clone it first! DecimalFormat isn't
-    // thread-safe.
-    /** "c" number format as it was starting from Incompatible Improvements 2.3.21. */
-    private static final DecimalFormat C_NUMBER_FORMAT_ICI_2_3_21 = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_20.clone();
-    static {
-        DecimalFormatSymbols symbols = C_NUMBER_FORMAT_ICI_2_3_21.getDecimalFormatSymbols();
-        symbols.setInfinity("INF");
-        symbols.setNaN("NaN");
-        C_NUMBER_FORMAT_ICI_2_3_21.setDecimalFormatSymbols(symbols);
-    }
-
     private final Configuration configuration;
     private final boolean incompatibleImprovementsGE2328;
     private final TemplateHashModel rootDataModel;
@@ -167,6 +143,16 @@ public final class Environment extends Configurable {
     @Deprecated
     private NumberFormat cNumberFormat;
     private TemplateNumberFormat cTemplateNumberFormat;
+    private TemplateNumberFormat cTemplateNumberFormatWithPre2331IcIBug;
+
+    /**
+     * Should be a boolean "trueAndFalseStringsCached", but with Incompatible Improvements less than 2.3.22 the
+     * effective value of {@code boolean_format} could change because of {@code #import} and {@code #include},
+     * as those changed the parent template. So we need this cache invalidation trick.
+     */
+    private Configurable trueAndFalseStringsCachedForParent;
+    private String cachedTrueString;
+    private String cachedFalseString;
 
     /**
      * Used by the "iso_" built-ins to accelerate formatting.
@@ -1631,6 +1617,8 @@ public final class Environment extends Configurable {
         return format;
     }
 
+    static final String COMPUTER_FORMAT_STRING = "computer";
+
     /**
      * Returns the {@link TemplateNumberFormat} for the given parameters without using the {@link Environment}-level
      * cache. Of course, the {@link TemplateNumberFormatFactory} involved might still uses its own cache.
@@ -1669,8 +1657,8 @@ public final class Environment extends Configurable {
 
             return formatFactory.get(params, locale, this);
         } else if (formatStringLen >= 1 && formatString.charAt(0) == 'c'
-                && (formatStringLen == 1 || formatString.equals(COMPUTER))) {
-            return getCTemplateNumberFormat();
+                && (formatStringLen == 1 || formatString.equals(COMPUTER_FORMAT_STRING))) {
+            return getCTemplateNumberFormatWithPre2331IcIBug();
         } else {
             return JavaTemplateNumberFormatFactory.INSTANCE.get(formatString, locale, this);
         }
@@ -1684,41 +1672,65 @@ public final class Environment extends Configurable {
      *
      * @deprecated Use {@link #getCTemplateNumberFormat()} instead. This method can't return the format used when
      * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is 2.3.32,
-     * or greater, and instead it will fall back to return the format that was used for 2.3.31.
+     * or greater, and instead it will fall back to return the format that was used for 2.3.31. Also, as its described
+     * earlier, this method was inconsistent with {@code ?c} between Incompatible Improvements 2.3.21 and 2.3.30, while
+     * {@link #getCTemplateNumberFormat()} behaves as {@code ?c} for all Incompatible Improvements value.
      */
     @Deprecated
     public NumberFormat getCNumberFormat() {
-        ensureCNumberFormatInitialized();
+        if (cNumberFormat == null) {
+            cNumberFormat = getCFormatWithPre2331IcIBug().getLegacyNumberFormat();
+        }
         return cNumberFormat;
     }
 
     /**
-     * Returns the {@link TemplateNumberFormat} used for the <tt>c</tt> built-in uses.
-     * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements} is 2.3.32 or greater.
+     * Returns the {@link TemplateNumberFormat} used for the <tt>c</tt> built-in currently uses in this environment.
+     * Calling this method for many times is fine, as it internally caches the result object.
+     * Remember that {@link TemplateNumberFormat}-s are not thread-safe objects, so the resulting object should only
+     * be used in the same thread where this {@link Environment} runs.
      *
      * @since 2.3.32
      */
     public TemplateNumberFormat getCTemplateNumberFormat() {
-        if (configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_32) {
-            ensureCNumberFormatInitialized();
-            return cTemplateNumberFormat;
+        if (cTemplateNumberFormat == null) {
+            cTemplateNumberFormat = getCFormat().getTemplateNumberFormat();
+        }
+        return cTemplateNumberFormat;
+    }
+
+    /**
+     * Like {@link #getCTemplateNumberFormat()}, but emulates the same bug as
+     * {@link #getCNumberFormat()} if a legacy default {@link CFormat} is used.
+     */
+    private TemplateNumberFormat getCTemplateNumberFormatWithPre2331IcIBug() {
+        if (cTemplateNumberFormatWithPre2331IcIBug == null) {
+            cTemplateNumberFormatWithPre2331IcIBug = getCFormatWithPre2331IcIBug().getTemplateNumberFormat();
         }
-        return CTemplateNumberFormat.INSTANCE;
+        return cTemplateNumberFormatWithPre2331IcIBug;
     }
 
-    static final String COMPUTER = "computer";
+    private CFormat getCFormatWithPre2331IcIBug() {
+        CFormat cFormat = getCFormat();
+        if (cFormat == Default2321CFormat.INSTANCE
+                && configuration.getIncompatibleImprovements().intValue() < _VersionInts.V_2_3_31) {
+            return Default230CFormat.INSTANCE;
+        }
+        return cFormat;
+    }
 
-    private void ensureCNumberFormatInitialized() {
-        // Note: DecimalFormat-s aren't thread-safe, so you must clone the static field value.
-        if (cNumberFormat == null) {
-            if (configuration.getIncompatibleImprovements().intValue() >= _VersionInts.V_2_3_31) {
-                cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_21.clone();
-            } else {
-                cNumberFormat = (DecimalFormat) C_NUMBER_FORMAT_ICI_2_3_20.clone();
+    @Override
+    public void setCFormat(CFormat cFormat) {
+        CFormat prevCFormat = getCFormat();
+        super.setCFormat(cFormat);
+        if (prevCFormat != cFormat) {
+            cTemplateNumberFormat = null;
+            cTemplateNumberFormatWithPre2331IcIBug = null;
+            if (cachedTemplateNumberFormats != null) {
+                cachedTemplateNumberFormats.remove(C_FORMAT_STRING);
+                cachedTemplateNumberFormats.remove(COMPUTER_FORMAT_STRING);
             }
-            // Note this uses the legacy name "computer", instead of "c". From IcI 2.3.32 we are using
-            // CTemplateNumberFormat.INSTANCE instead, so users won't see this anymore.
-            cTemplateNumberFormat = new JavaTemplateNumberFormat(cNumberFormat, COMPUTER);
+            clearCachedTrueAndFalseString();
         }
     }
 
@@ -1761,6 +1773,116 @@ public final class Environment extends Configurable {
         }
     }
 
+    @Override
+    public void setBooleanFormat(String booleanFormat) {
+        super.setBooleanFormat(booleanFormat);
+        clearCachedTrueAndFalseString();
+    }
+
+    String formatBoolean(boolean value, boolean fallbackToTrueFalse) throws TemplateException {
+        if (value) {
+            String s = getTrueStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_TRUE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        } else {
+            String s = getFalseStringValue();
+            if (s == null) {
+                if (fallbackToTrueFalse) {
+                    return MiscUtil.C_FALSE;
+                } else {
+                    throw new _MiscTemplateException(getNullBooleanFormatErrorDescription());
+                }
+            } else {
+                return s;
+            }
+        }
+    }
+
+    private _ErrorDescriptionBuilder getNullBooleanFormatErrorDescription() {
+        return new _ErrorDescriptionBuilder(
+                "Can't convert boolean to string automatically, because the \"", BOOLEAN_FORMAT_KEY ,"\" setting was ",
+                new _DelayedJQuote(getBooleanFormat()),
+                (getBooleanFormat().equals(C_TRUE_FALSE)
+                        ? ", which is the legacy deprecated default, and we treat it as if no format was set. "
+                        + "This is the default configuration; you should provide the format explicitly for each "
+                        + "place where you print a boolean."
+                        : ".")
+        ).tips(
+                "Write something like myBool?string('yes', 'no') to specify boolean formatting in place.",
+                new Object[]{
+                        "If you want \"true\"/\"false\" result as you are generating computer-language output "
+                                + "(not for direct human consumption), then use \"?c\", like ${myBool?c}. (If you "
+                                + "always generate computer-language output, then it's might be reasonable to set "
+                                + "the \"", BOOLEAN_FORMAT_KEY, "\" setting to \"c\" instead.)",
+                },
+                new Object[] {
+                        "If you need the same two values on most places, the programmers can set the \"",
+                        BOOLEAN_FORMAT_KEY ,"\" setting to something like \"yes,no\". However, then it will be easy to "
+                        + "unwillingly format booleans like that."
+                }
+        );
+    }
+
+    /**
+     * Returns the string to which {@code true} is converted to for human audience, or {@code null} if automatic
+     * coercion to string is not allowed.
+     *
+     * <p>This value is deduced from the {@code "boolean_format"} setting.
+     * Confusingly, for backward compatibility (at least until 2.4) that defaults to {@code "true,false"}, yet this
+     * defaults to {@code null}. That's so because {@code "true,false"} is treated exceptionally, as that default is a
+     * historical mistake in FreeMarker, since it targets computer language output, not human writing. Thus it's
+     * ignored, and instead we admit that we don't know how to show boolean values.
+     */
+    String getTrueStringValue() {
+        if (trueAndFalseStringsCachedForParent == getParent()) {
+            return cachedTrueString;
+        }
+        cacheTrueAndFalseStrings();
+        return cachedTrueString;
+    }
+
+    /**
+     * Same as {@link #getTrueStringValue()} but with {@code false}.
+     */
+    String getFalseStringValue() {
+        if (trueAndFalseStringsCachedForParent == getParent()) {
+            return cachedFalseString;
+        }
+        cacheTrueAndFalseStrings();
+        return cachedFalseString;
+    }
+
+    private void clearCachedTrueAndFalseString() {
+        trueAndFalseStringsCachedForParent = null;
+        cachedTrueString = null;
+        cachedFalseString = null;
+    }
+
+    private void cacheTrueAndFalseStrings() {
+        String spitTrueSide = getBooleanFormatCommaSplitTrueSide();
+        if (spitTrueSide != null) {
+            cachedTrueString = spitTrueSide;
+            cachedFalseString = getBooleanFormatCommaSplitFalseSide();
+        } else if (getBooleanFormat().equals(C_FORMAT_STRING)) {
+            CFormat cFormat = getCFormat();
+            cachedTrueString = cFormat.getTrueString();
+            cachedFalseString = cFormat.getFalseString();
+        } else {
+            // This happens for C_TRUE_FALSE deliberately. That's the default for BC, but it's not a good default for human
+            // audience formatting, so we pretend that it wasn't set.
+            cachedTrueString = null;
+            cachedFalseString = null;
+        }
+        trueAndFalseStringsCachedForParent = getParent();
+    }
+
     public Configuration getConfiguration() {
         return configuration;
     }
diff --git a/src/main/java/freemarker/core/EvalUtil.java b/src/main/java/freemarker/core/EvalUtil.java
index 3d3f900d..1b469c93 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -481,7 +481,7 @@ class EvalUtil {
                     throw InvalidReferenceException.getInstance(exp, env);
                 } else {
                     throw new InvalidReferenceException(
-                            "Null/missing value (no more informatoin avilable)",
+                            "Null/missing value (no more information available)",
                             env);
                 }
             }
diff --git a/src/main/java/freemarker/core/JSONCFormat.java b/src/main/java/freemarker/core/JSONCFormat.java
new file mode 100644
index 00000000..5cf4c52a
--- /dev/null
+++ b/src/main/java/freemarker/core/JSONCFormat.java
@@ -0,0 +1,56 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.Version;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * JSON {@link CFormat}; when this is used, values output by {@code ?c} are valid JSON values, and therefore also
+ * valid JavaScript values.
+ * This is the default of {@link Configurable#getCFormat()} starting from
+ * {@linkplain Configuration#setIncompatibleImprovements(Version) Incompatible Improvements}
+ * {@link Configuration#VERSION_2_3_32}.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public final class JSONCFormat extends AbstractJSONLikeFormat {
+    public static final String NAME = "JSON";
+    public static final JSONCFormat INSTANCE = new JSONCFormat();
+
+    private JSONCFormat() {
+    }
+
+    @Override
+    String formatString(String s, Environment env) throws TemplateException {
+        return StringUtil.jsStringEnc(s, true, true);
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/JavaCFormat.java b/src/main/java/freemarker/core/JavaCFormat.java
new file mode 100644
index 00000000..6a1d0d61
--- /dev/null
+++ b/src/main/java/freemarker/core/JavaCFormat.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * Java {@link CFormat}.
+ *
+ * @since 2.3.32
+ */
+public final class JavaCFormat extends CFormat {
+    public static final String NAME = "Java";
+    public static final JavaCFormat INSTANCE = new JavaCFormat();
+
+    private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+            "Double.POSITIVE_INFINITY", "Double.NEGATIVE_INFINITY", "Double.NaN",
+            "Float.POSITIVE_INFINITY", "Float.NEGATIVE_INFINITY", "Float.NaN");
+
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+    static {
+        DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+        symbols.setInfinity("Double.POSITIVE_INFINITY");
+        symbols.setNaN("Double.NaN");
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+    }
+
+    private JavaCFormat() {
+    }
+
+    @Override
+    TemplateNumberFormat getTemplateNumberFormat() {
+        return TEMPLATE_NUMBER_FORMAT;
+    }
+
+    @Override
+    String formatString(String s, Environment env) throws TemplateException {
+        return StringUtil.javaStringEnc(s, true);
+    }
+
+    @Override
+    String getTrueString() {
+        return "true";
+    }
+
+    @Override
+    String getFalseString() {
+        return "false";
+    }
+
+    @Override
+    String getNullString() {
+        return "null";
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/JavaScriptCFormat.java b/src/main/java/freemarker/core/JavaScriptCFormat.java
new file mode 100644
index 00000000..d9ba7229
--- /dev/null
+++ b/src/main/java/freemarker/core/JavaScriptCFormat.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+/**
+ * JavaScript {@link CFormat}. This is almost the same as {@link JSONCFormat}, but it uses shorter forms where
+ * the additional JavaScript features make that possible.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ *
+ * @since 2.3.32
+ */
+public final class JavaScriptCFormat extends AbstractJSONLikeFormat {
+    public static final String NAME = "JavaScript";
+    public static final JavaScriptCFormat INSTANCE = new JavaScriptCFormat();
+
+    private JavaScriptCFormat() {
+    }
+
+    @Override
+    String formatString(String s, Environment env) throws TemplateException {
+        return StringUtil.jsStringEnc(s, false, true);
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
index 59e3288c..5280bb39 100644
--- a/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
+++ b/src/main/java/freemarker/core/JavaTemplateNumberFormat.java
@@ -23,7 +23,7 @@ import java.text.NumberFormat;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 
-final class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFormat {
+class JavaTemplateNumberFormat extends BackwardCompatibleTemplateNumberFormat {
     
     private final String formatString;
     private final NumberFormat javaNumberFormat;
diff --git a/src/main/java/freemarker/core/PropertySetting.java b/src/main/java/freemarker/core/PropertySetting.java
index 0c2e22ab..117a8c00 100644
--- a/src/main/java/freemarker/core/PropertySetting.java
+++ b/src/main/java/freemarker/core/PropertySetting.java
@@ -38,11 +38,14 @@ final class PropertySetting extends TemplateElement {
 
     private final String key;
     private final Expression value;
+    private final ValueSafetyChecker valueSafeyChecker;
     
     static final String[] SETTING_NAMES = new String[] {
             // Must be sorted alphabetically!
             Configurable.BOOLEAN_FORMAT_KEY_CAMEL_CASE,
             Configurable.BOOLEAN_FORMAT_KEY_SNAKE_CASE,
+            Configurable.C_FORMAT_KEY_CAMEL_CASE,
+            Configurable.C_FORMAT_KEY_SNAKE_CASE,
             Configurable.CLASSIC_COMPATIBLE_KEY_CAMEL_CASE,
             Configurable.CLASSIC_COMPATIBLE_KEY_SNAKE_CASE,
             Configurable.DATE_FORMAT_KEY_CAMEL_CASE,
@@ -66,7 +69,7 @@ final class PropertySetting extends TemplateElement {
 
     PropertySetting(Token keyTk, FMParserTokenManager tokenManager, Expression value, Configuration cfg)
             throws ParseException {
-        String key = keyTk.image;
+        final String key = keyTk.image;
         if (Arrays.binarySearch(SETTING_NAMES, key) < 0) {
             StringBuilder sb = new StringBuilder();
             if (_TemplateAPI.getConfigurationSettingNames(cfg, true).contains(key)
@@ -107,6 +110,26 @@ final class PropertySetting extends TemplateElement {
         
         this.key = key;
         this.value = value;
+
+        if (key.equals(Configurable.C_FORMAT_KEY_SNAKE_CASE) || key.equals(Configurable.C_FORMAT_KEY_CAMEL_CASE)) {
+            valueSafeyChecker = new ValueSafetyChecker() {
+                @Override
+                public void check(Environment env, String actualValue) throws TemplateException {
+                    if (actualValue.startsWith("@")
+                            || StandardCFormats.STANDARD_C_FORMATS.containsKey(actualValue)
+                            || actualValue.equals("default")) {
+                        return;
+                    }
+                    throw new TemplateException("It's not allowed to set \"" + key + "\" to "
+                            + StringUtil.jQuote(actualValue) + " in a template. Use a standard c format name ("
+                            + String.join(", ", StandardCFormats.STANDARD_C_FORMATS.keySet()) + "), " +
+                            "or registered custom  c format name after a \"@\".",
+                            env);
+                }
+            };
+        } else {
+            valueSafeyChecker = null;
+        }
     }
 
     @Override
@@ -122,6 +145,9 @@ final class PropertySetting extends TemplateElement {
         } else {
             strval = value.evalAndCoerceToStringOrUnsupportedMarkup(env);
         }
+        if (valueSafeyChecker != null) {
+            valueSafeyChecker.check(env, strval);
+        }
         env.setSetting(key, strval);
         return null;
     }
@@ -171,5 +197,9 @@ final class PropertySetting extends TemplateElement {
     boolean isNestedBlockRepeater() {
         return false;
     }
+
+    private interface ValueSafetyChecker {
+        void check(Environment env, String value) throws TemplateException;
+    }
     
 }
diff --git a/src/main/java/freemarker/core/TemplateConfiguration.java b/src/main/java/freemarker/core/TemplateConfiguration.java
index ff290f5b..121c18b6 100644
--- a/src/main/java/freemarker/core/TemplateConfiguration.java
+++ b/src/main/java/freemarker/core/TemplateConfiguration.java
@@ -191,6 +191,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
         if (tc.isDateTimeFormatSet()) {
             setDateTimeFormat(tc.getDateTimeFormat());
         }
+        if (tc.isCFormatSet()) {
+            setCFormat(tc.getCFormat());
+        }
         if (tc.isEncodingSet()) {
             setEncoding(tc.getEncoding());
         }
@@ -333,6 +336,9 @@ public final class TemplateConfiguration extends Configurable implements ParserC
         if (isDateTimeFormatSet() && !template.isDateTimeFormatSet()) {
             template.setDateTimeFormat(getDateTimeFormat());
         }
+        if (isCFormatSet() && !template.isCFormatSet()) {
+            template.setCFormat(getCFormat());
+        }
         if (isEncodingSet() && template.getEncoding() == null) {
             template.setEncoding(getEncoding());
         }
@@ -541,7 +547,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
     public boolean isOutputFormatSet() {
         return outputFormat != null;
     }
-    
+
     /**
      * See {@link Configuration#setRecognizeStandardFileExtensions(boolean)}. 
      */
@@ -680,6 +686,7 @@ public final class TemplateConfiguration extends Configurable implements ParserC
                 || isCustomNumberFormatsSet()
                 || isDateFormatSet()
                 || isDateTimeFormatSet()
+                || isCFormatSet()
                 || isLazyImportsSet()
                 || isLazyAutoImportsSet()
                 || isLocaleSet()
diff --git a/src/main/java/freemarker/core/XSCFormat.java b/src/main/java/freemarker/core/XSCFormat.java
new file mode 100644
index 00000000..c3ee5774
--- /dev/null
+++ b/src/main/java/freemarker/core/XSCFormat.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+
+/**
+ * {@link CFormat} for outputting XML that follows the conventions of XML Schema.
+ *
+ * <p><b>Experimental class!</b> This class is too new, and might will change over time. Therefore, for now the
+ * most methods are not exposed outside FreeMarker. The class itself and some members are exposed as they are needed for
+ * configuring FreeMarker.
+ */
+public final class XSCFormat extends CFormat {
+    public static final String NAME = "XS";
+    public static final XSCFormat INSTANCE = new XSCFormat();
+
+    private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+            "INF", "-INF", "NaN",
+            "INF", "-INF", "NaN");
+
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE = (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+    static {
+        DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+        symbols.setInfinity("INF");
+        symbols.setNaN("NaN");
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+
+    private XSCFormat() {
+    }
+
+    @Override
+    TemplateNumberFormat getTemplateNumberFormat() {
+        return TEMPLATE_NUMBER_FORMAT;
+    }
+
+    @Override
+    String formatString(String s, Environment env) throws TemplateException {
+        return s; // So we don't escape here, as we assume that there's XML auto-escaping
+    }
+
+    @Override
+    String getTrueString() {
+        return "true";
+    }
+
+    @Override
+    String getFalseString() {
+        return "false";
+    }
+
+    @Override
+    String getNullString() {
+        // XSD has no null literal, and you have to leave out the whole element to represent null. We can achieve that
+        // here. The closet we can do is causing an empty element or attribute to be outputted. Some frameworks will
+        // interpret that as null if the XSD type is not string.
+        return "";
+    }
+
+    @Override
+    public String getName() {
+        return NAME;
+    }
+}
diff --git a/src/main/java/freemarker/core/_StandardCLanguages.java b/src/main/java/freemarker/core/_StandardCLanguages.java
new file mode 100644
index 00000000..37da6381
--- /dev/null
+++ b/src/main/java/freemarker/core/_StandardCLanguages.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+final class StandardCFormats {
+    private StandardCFormats() {
+    }
+
+    static final Map<String, CFormat> STANDARD_C_FORMATS;
+    static {
+            STANDARD_C_FORMATS = new LinkedHashMap<>();
+            STANDARD_C_FORMATS.put(JSONCFormat.INSTANCE.getName(), JSONCFormat.INSTANCE);
+            STANDARD_C_FORMATS.put(JavaScriptCFormat.INSTANCE.getName(), JavaScriptCFormat.INSTANCE);
+            STANDARD_C_FORMATS.put(JavaCFormat.INSTANCE.getName(), JavaCFormat.INSTANCE);
+            STANDARD_C_FORMATS.put(XSCFormat.INSTANCE.getName(), XSCFormat.INSTANCE);
+            STANDARD_C_FORMATS.put(Default230CFormat.INSTANCE.getName(), Default230CFormat.INSTANCE);
+            STANDARD_C_FORMATS.put(Default2321CFormat.INSTANCE.getName(), Default2321CFormat.INSTANCE);
+    }
+
+}
diff --git a/src/main/java/freemarker/template/Configuration.java b/src/main/java/freemarker/template/Configuration.java
index 93bfd83d..1ffa86f6 100644
--- a/src/main/java/freemarker/template/Configuration.java
+++ b/src/main/java/freemarker/template/Configuration.java
@@ -59,11 +59,15 @@ import freemarker.cache.TemplateLookupStrategy;
 import freemarker.cache.TemplateNameFormat;
 import freemarker.cache.URLTemplateLoader;
 import freemarker.core.BugException;
+import freemarker.core.CFormat;
 import freemarker.core.CSSOutputFormat;
 import freemarker.core.CombinedMarkupOutputFormat;
 import freemarker.core.Configurable;
+import freemarker.core.Default230CFormat;
+import freemarker.core.Default2321CFormat;
 import freemarker.core.Environment;
 import freemarker.core.HTMLOutputFormat;
+import freemarker.core.JSONCFormat;
 import freemarker.core.JSONOutputFormat;
 import freemarker.core.JavaScriptOutputFormat;
 import freemarker.core.MarkupOutputFormat;
@@ -384,7 +388,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
         STANDARD_OUTPUT_FORMATS.put(JavaScriptOutputFormat.INSTANCE.getName(), JavaScriptOutputFormat.INSTANCE);
         STANDARD_OUTPUT_FORMATS.put(JSONOutputFormat.INSTANCE.getName(), JSONOutputFormat.INSTANCE);
     }
-    
+
     /**
      * The parser decides between {@link #ANGLE_BRACKET_TAG_SYNTAX} and {@link #SQUARE_BRACKET_TAG_SYNTAX} based on the
      * first tag (like {@code [#if x]} or {@code <#if x>}) it mets. Note that {@code [=...]} is <em>not</em> a tag, but
@@ -572,6 +576,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     private boolean localeExplicitlySet;
     private boolean defaultEncodingExplicitlySet;
     private boolean timeZoneExplicitlySet;
+    private boolean cFormatExplicitlySet;
 
     
     private HashMap/*<String, TemplateModel>*/ sharedVariables = new HashMap();
@@ -739,7 +744,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
      *          It won't be affected by {@code #include} and {@code #nested} anymore. This is unintended, a bug with
      *          {@code incompatible_improvement} 2.3.22 (a consequence of the lower level fixing described in the next
      *          point). The old behavior of {@code .template_name} is restored if you set
-     *          {@code incompatible_improvement} to 2.3.23 (while {@link Configurable#getParent()}) of
+     *          {@code incompatible_improvement} to 2.3.23 (while {@link Configurable#getParent()} of
      *          {@link Environment} keeps the changed behavior shown in the next point). 
      *       </li>
      *       <li><p>
@@ -1824,7 +1829,7 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
     static TimeZone getDefaultTimeZone() {
         return TimeZone.getDefault();
     }
-    
+
     @Override
     public void setTemplateExceptionHandler(TemplateExceptionHandler templateExceptionHandler) {
         super.setTemplateExceptionHandler(templateExceptionHandler);
@@ -2019,7 +2024,12 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 logTemplateExceptionsExplicitlySet = true;
                 unsetLogTemplateExceptions();
             }
-            
+
+            if (!cFormatExplicitlySet) {
+                cFormatExplicitlySet = true;
+                unsetCFormat();
+            }
+
             if (!wrapUncheckedExceptionsExplicitlySet) {
                 wrapUncheckedExceptionsExplicitlySet = true;
                 unsetWrapUncheckedExceptions();
@@ -2467,6 +2477,45 @@ public class Configuration extends Configurable implements Cloneable, ParserConf
                 : recognizeStandardFileExtensions.booleanValue();
     }
 
+    @Override
+    public void setCFormat(CFormat cFormat) {
+        super.setCFormat(cFormat);
+        cFormatExplicitlySet = true;
+    }
+
+    /**
+     * Resets the setting to its default, as if it was never set. This means that when you change the
+     * {@code incompatibe_improvements} setting later, the default will also change as appropriate. Also
+     * {@link #isCFormatExplicitlySet()} will return {@code false}.
+     *
+     * @since 2.3.32
+     */
+    public void unsetCFormat() {
+        if (cFormatExplicitlySet) {
+            setCFormat(getDefaultCFormat(incompatibleImprovements));
+            cFormatExplicitlySet = false;
+        }
+    }
+
+    static CFormat getDefaultCFormat(Version incompatibleImprovements) {
+        if (incompatibleImprovements.intValue() >= _VersionInts.V_2_3_32) {
+            return JSONCFormat.INSTANCE;
+        }
+        if (incompatibleImprovements.intValue() >= _VersionInts.V_2_3_21) {
+            return Default2321CFormat.INSTANCE;
+        }
+        return Default230CFormat.INSTANCE;
+    }
+
+    /**
+     * Tells if {@link #setCFormat(CFormat)} (or equivalent) was already called on this instance.
+     *
+     * @since 2.3.32
+     */
+    public boolean isCFormatExplicitlySet() {
+        return cFormatExplicitlySet;
+    }
+
     /**
      * Determines the tag syntax (like {@code <#if x>} VS {@code [#if x]}) of the template files 
      * that has no {@code #ftl} header to decide that. Don't confuse this with the interpolation syntax
diff --git a/src/main/java/freemarker/template/_TemplateAPI.java b/src/main/java/freemarker/template/_TemplateAPI.java
index 51002b8b..0f7f23b4 100644
--- a/src/main/java/freemarker/template/_TemplateAPI.java
+++ b/src/main/java/freemarker/template/_TemplateAPI.java
@@ -27,6 +27,7 @@ import freemarker.cache.CacheStorage;
 import freemarker.cache.TemplateLoader;
 import freemarker.cache.TemplateLookupStrategy;
 import freemarker.cache.TemplateNameFormat;
+import freemarker.core.CFormat;
 import freemarker.core.Expression;
 import freemarker.core.OutputFormat;
 import freemarker.core.TemplateObject;
@@ -185,11 +186,15 @@ public class _TemplateAPI {
     public static Locale getDefaultLocale() {
         return Configuration.getDefaultLocale();
     }
-    
+
     public static TimeZone getDefaultTimeZone() {
         return Configuration.getDefaultTimeZone();
     }
 
+    public static CFormat getDefaultCFormat(Version incompatibleImprovements) {
+        return Configuration.getDefaultCFormat(incompatibleImprovements);
+    }
+
     public static void setPreventStrippings(Configuration conf, boolean preventStrippings) {
         conf.setPreventStrippings(preventStrippings);
     }
diff --git a/src/main/java/freemarker/template/utility/StringUtil.java b/src/main/java/freemarker/template/utility/StringUtil.java
index 33179556..0d76a48f 100644
--- a/src/main/java/freemarker/template/utility/StringUtil.java
+++ b/src/main/java/freemarker/template/utility/StringUtil.java
@@ -881,39 +881,7 @@ public class StringUtil {
         if (s == null) {
             return "null";
         }
-        int ln = s.length();
-        StringBuilder b = new StringBuilder(ln + 4);
-        b.append('"');
-        for (int i = 0; i < ln; i++) {
-            char c = s.charAt(i);
-            if (c == '"') {
-                b.append("\\\"");
-            } else if (c == '\\') {
-                b.append("\\\\");
-            } else if (c < 0x20) {
-                if (c == '\n') {
-                    b.append("\\n");
-                } else if (c == '\r') {
-                    b.append("\\r");
-                } else if (c == '\f') {
-                    b.append("\\f");
-                } else if (c == '\b') {
-                    b.append("\\b");
-                } else if (c == '\t') {
-                    b.append("\\t");
-                } else {
-                    b.append("\\u00");
-                    int x = c / 0x10;
-                    b.append(toHexDigit(x));
-                    x = c & 0xF;
-                    b.append(toHexDigit(x));
-                }
-            } else {
-                b.append(c);
-            }
-        } // for each characters
-        b.append('"');
-        return b.toString();
+        return javaStringEnc(s, true);
     }
 
     /**
@@ -934,7 +902,7 @@ public class StringUtil {
             return "null";
         }
         int ln = s.length();
-        StringBuilder b = new StringBuilder(ln + 4);
+        StringBuilder b = new StringBuilder(ln + 6);
         b.append('"');
         for (int i = 0; i < ln; i++) {
             char c = s.charAt(i);
@@ -957,10 +925,8 @@ public class StringUtil {
                     b.append("\\t");
                 } else {
                     b.append("\\u00");
-                    int x = c / 0x10;
-                    b.append(toHexDigit(x));
-                    x = c & 0xF;
-                    b.append(toHexDigit(x));
+                    b.append(toHexDigitLowerCase(c / 0x10));
+                    b.append(toHexDigitLowerCase(c & 0xF));
                 }
             } else {
                 b.append(c);
@@ -1281,25 +1247,39 @@ public class StringUtil {
     public static boolean isBackslashEscapedFTLIdentifierCharacter(final char c) {
         return c == '-' || c == '.' || c == ':' || c ==  '#';
     }
-    
+
     /**
-     * Escapes the <code>String</code> with the escaping rules of Java language
+     * Escapes the {@code String} with the escaping rules of Java language
      * string literals, so it's safe to insert the value into a string literal.
      * The resulting string will not be quoted.
-     * 
+     *
+     * See more details at {@link #javaStringEnc(String, boolean)}, as this just calls that with {@code false} as the
+     * 2nd argument.
+     */
+    public static String javaStringEnc(String s) {
+        return javaStringEnc(s, false);
+    }
+
+    /**
+     * Escapes the {@code String} with the escaping rules of Java language string literals, and then if {@code quote} is
+     * true, it also adds quotation marks before and after it.
+     *
      * <p>All characters under UCS code point 0x20 will be escaped.
      * Where they have no dedicated escape sequence in Java, they will
      * be replaced with hexadecimal escape (<tt>\</tt><tt>u<i>XXXX</i></tt>). 
      * 
      * @see #jQuote(String)
      */ 
-    public static String javaStringEnc(String s) {
+    public static String javaStringEnc(String s, boolean quote) {
         int ln = s.length();
         for (int i = 0; i < ln; i++) {
             char c = s.charAt(i);
             if (c == '"' || c == '\\' || c < 0x20) {
-                StringBuilder b = new StringBuilder(ln + 4);
-                b.append(s.substring(0, i));
+                StringBuilder b = new StringBuilder(ln + (quote ? 6 : 4));
+                if (quote) {
+                    b.append("\"");
+                }
+                b.append(s, 0, i);
                 while (true) {
                     if (c == '"') {
                         b.append("\\\"");
@@ -1318,30 +1298,29 @@ public class StringUtil {
                             b.append("\\t");
                         } else {
                             b.append("\\u00");
-                            int x = c / 0x10;
-                            b.append((char)
-                                    (x < 0xA ? x + '0' : x - 0xA + 'a'));
-                            x = c & 0xF;
-                            b.append((char)
-                                    (x < 0xA ? x + '0' : x - 0xA + 'a'));
+                            b.append(toHexDigitLowerCase(c / 0x10));
+                            b.append(toHexDigitLowerCase(c & 0xF));
                         }
                     } else {
                         b.append(c);
                     }
                     i++;
                     if (i >= ln) {
+                        if (quote) {
+                            b.append("\"");
+                        }
                         return b.toString();
                     }
                     c = s.charAt(i);
                 }
             } // if has to be escaped
         } // for each characters
-        return s;
+        return quote ? '"' + s + '"' : s;
     }
     
     /**
      * Escapes a {@link String} to be safely insertable into a JavaScript string literal; for more see
-     * {@link #jsStringEnc(String, boolean) jsStringEnc(s, false)}.
+     * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, false, false)}.
      */
     public static String javaScriptStringEnc(String s) {
         return jsStringEnc(s, false);
@@ -1360,20 +1339,30 @@ public class StringUtil {
     private static final int ESC_BACKSLASH = 3;
     
     /**
-     * Escapes a {@link String} to be safely insertable into a JavaScript or a JSON string literal.
-     * The resulting string will <em>not</em> be quoted; the caller must ensure that they are there in the final
-     * output. Note that for JSON, the quotation marks must be {@code "}, not {@code '}, because JSON doesn't escape
-     * {@code '}.
-     * 
+     * Escapes a {@link String} to be safely insertable into a JSON or JavaScript string literal; for more see
+     * {@link #jsStringEnc(String, boolean, boolean) jsStringEnc(s, json, false)}.
+     *
+     * @since 2.3.20
+     */
+    public static String jsStringEnc(String s, boolean json) {
+        return jsStringEnc(s, json, false);
+    }
+
+    /**
+     * Escapes a {@link String} to be safely insertable into a JavaScript or a JSON string literal, and if the 3rd
+     * argument is {@code true}, also adds quotation marks around it.
+     * If instead the caller ensures that the quotation marks are there, then in JSON mode (2nd argument), the quotation
+     * marks must be {@code "}, not {@code '}, because for JSON we won't escape {@code '}.
+     *
      * <p>The escaping rules guarantee that if the inside of the JavaScript/JSON string literal is from one or more
      * touching pieces that were escaped with this, no character sequence can occur that closes the
      * JavaScript/JSON string literal, or has a meaning in HTML/XML that causes the HTML script section to be closed.
      * (If, however, the escaped section is preceded by or followed by strings from other sources, this can't be
      * guaranteed in some rare cases. Like <tt>x = "&lt;/${a?js_string}"</tt> might closes the "script"
      * element if {@code a} is {@code "script>"}.)
-     * 
+     *
      * The escaped characters are:
-     * 
+     *
      * <table style="width: auto; border-collapse: collapse" border="1" summary="Characters escaped by jsStringEnc">
      * <tr>
      *   <th>Input
@@ -1382,7 +1371,7 @@ public class StringUtil {
      *   <td><tt>"</tt>
      *   <td><tt>\"</tt>
      * <tr>
-     *   <td><tt>'</tt> if not in JSON-mode
+     *   <td><tt>'</tt> if not in JSON-mode, nor is the {@code quited} argument {@code true}
      *   <td><tt>\'</tt>
      * <tr>
      *   <td><tt>\</tt>
@@ -1394,7 +1383,7 @@ public class StringUtil {
      *   <td><tt>&gt;</tt> if the method can't know that it won't be directly after <tt>]]</tt> or <tt>--</tt>
      *   <td>JavaScript: <tt>\&gt;</tt>; JSON: <tt>\</tt><tt>u003E</tt>
      * <tr>
-     *   <td><tt>&lt;</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt> 
+     *   <td><tt>&lt;</tt> if the method can't know that it won't be directly followed by <tt>!</tt> or <tt>?</tt>
      *   <td><tt><tt>\</tt>u003C</tt>
      * <tr>
      *   <td>
@@ -1408,14 +1397,25 @@ public class StringUtil {
      *     u2029 (Paragraph separator - source code line-break in ECMAScript)<br>
      *   <td><tt>\<tt>u</tt><i>XXXX</i></tt>
      * </table>
-     * 
-     * @since 2.3.20
+     *
+     * @param s The string to escape
+     * @param json If escaping should restrict itself to rules that are valid in both JSON and JavaScript.
+     * @param quote If quotation marks should be added around the result.
+     *      Currently, it's always ({@code "}, not {@code '}).
+     *
+     * @since 2.3.32
      */
-    public static String jsStringEnc(String s, boolean json) {
+    public static String jsStringEnc(String s, boolean json, boolean quote) {
         NullArgumentException.check("s", s);
         
         int ln = s.length();
-        StringBuilder sb = null;
+        StringBuilder sb;
+        if (quote) {
+            sb = new StringBuilder(ln + 8);
+            sb.append('"');
+        } else {
+            sb = null;
+        }
         for (int i = 0; i < ln; i++) {
             final char c = s.charAt(i);
             final int escapeType;  // 
@@ -1437,15 +1437,17 @@ public class StringUtil {
                 } else if (c == '"') {
                     escapeType = ESC_BACKSLASH;
                 } else if (c == '\'') {
-                    escapeType = json ? NO_ESC : ESC_BACKSLASH; 
+                    escapeType = json || quote ? NO_ESC : ESC_BACKSLASH;
                 } else if (c == '\\') {
                     escapeType = ESC_BACKSLASH; 
-                } else if (c == '/' && (i == 0 || s.charAt(i - 1) == '<')) {  // against closing elements
-                    escapeType = ESC_BACKSLASH; 
+                } else if (c == '/'  && (i == 0 || s.charAt(i - 1) == '<')) {  // against closing elements
+                    escapeType = quote ? NO_ESC : ESC_BACKSLASH;
                 } else if (c == '>') {  // against "]]> and "-->"
                     final boolean dangerous;
                     if (i == 0) {
                         dangerous = true;
+                    } else if (quote) {
+                        dangerous = false;
                     } else {
                         final char prevC = s.charAt(i - 1);
                         if (prevC == ']' || prevC == '-') {
@@ -1464,6 +1466,8 @@ public class StringUtil {
                     final boolean dangerous;
                     if (i == ln - 1) {
                         dangerous = true;
+                    } else if (quote) {
+                        dangerous = false;
                     } else {
                         char nextC = s.charAt(i + 1);
                         dangerous = nextC == '!' || nextC == '?';
@@ -1480,7 +1484,7 @@ public class StringUtil {
                 if (escapeType != NO_ESC) { // If needs escaping
                     if (sb == null) {
                         sb = new StringBuilder(ln + 6);
-                        sb.append(s.substring(0, i));
+                        sb.append(s, 0, i);
                     }
                     
                     sb.append('\\');
@@ -1489,15 +1493,15 @@ public class StringUtil {
                     } else if (escapeType == ESC_HEXA) {
                         if (!json && c < 0x100) {
                             sb.append('x');
-                            sb.append(toHexDigit(c >> 4));
-                            sb.append(toHexDigit(c & 0xF));
+                            sb.append(toHexDigitUpperCase(c >> 4));
+                            sb.append(toHexDigitUpperCase(c & 0xF));
                         } else {
                             sb.append('u');
                             int cp = c;
-                            sb.append(toHexDigit((cp >> 12) & 0xF));
-                            sb.append(toHexDigit((cp >> 8) & 0xF));
-                            sb.append(toHexDigit((cp >> 4) & 0xF));
-                            sb.append(toHexDigit(cp & 0xF));
+                            sb.append(toHexDigitUpperCase((cp >> 12) & 0xF));
+                            sb.append(toHexDigitUpperCase((cp >> 8) & 0xF));
+                            sb.append(toHexDigitUpperCase((cp >> 4) & 0xF));
+                            sb.append(toHexDigitUpperCase(cp & 0xF));
                         }
                     } else {  // escapeType == ESC_BACKSLASH
                         sb.append(c);
@@ -1510,14 +1514,22 @@ public class StringUtil {
                 
             if (sb != null) sb.append(c);
         } // for each characters
+
+        if (quote) {
+            sb.append('"');
+        }
         
         return sb == null ? s : sb.toString();
     }
 
-    private static char toHexDigit(int d) {
+    private static char toHexDigitLowerCase(int d) {
+        return (char) (d < 0xA ? d + '0' : d - 0xA + 'a');
+    }
+
+    private static char toHexDigitUpperCase(int d) {
         return (char) (d < 0xA ? d + '0' : d - 0xA + 'A');
     }
-    
+
     /**
      * Parses a name-value pair list, where the pairs are separated with comma,
      * and the name and value is separated with colon.
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 2198ffc1..8faa1a74 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -20,10 +20,7 @@
 <book conformance="docgen" version="5.0" xml:lang="en"
       xmlns="http://docbook.org/ns/docbook"
       xmlns:xlink="http://www.w3.org/1999/xlink"
-      xmlns:ns5="http://www.w3.org/2000/svg"
-      xmlns:ns4="http://www.w3.org/1998/Math/MathML"
-      xmlns:ns3="http://www.w3.org/1999/xhtml"
-      xmlns:ns="http://docbook.org/ns/docbook">
+>
   <info>
     <title>Apache FreeMarker Manual</title>
 
@@ -6013,6 +6010,119 @@ To prove that "s" didn't contain the value in escaped form:
         </section>
       </section>
 
+      <section xml:id="dgui_misc_computer_vs_human_format">
+        <title>Formatting for humans, or for computers</title>
+
+        <para>By default
+        <literal>${<replaceable>someValue</replaceable>}</literal> outputs
+        numbers, date/time/date-time and boolean values in a format that
+        targets normal users (<quote>humans</quote>). You have various
+        settings to specify how that format looks, like
+        <literal>number_format</literal>, <literal>date_format</literal>,
+        <literal>time_format</literal>, <literal>datetime_format</literal>,
+        <literal>boolan_format</literal>. The output also often depends on the
+        <literal>locale</literal> setting (i.e., on the language, and country
+        of the user). So 3000000 is possibly printed as
+        <literal>3,000,000</literal> (i.e., with grouping separators), or
+        <literal>3.14</literal> is possibly printed as <literal>3,14</literal>
+        (i.e., with a different decimal separator).</para>
+
+        <para>At some places you need to output values that will be read
+        (parsed) by some program, in which case always use the <link
+        linkend="ref_builtin_c"><literal>c</literal> built-in</link>, as in
+        <literal>${<replaceable>someValue</replaceable>?c}</literal> (the
+        <quote>c</quote> stands for Computer). <emphasis>Then the formatting
+        depends on the <literal>c_format</literal> <link
+        linkend="pgui_config_settings">setting</link></emphasis>, which
+        usually refers to a computer language, like <literal>"JSON"</literal>.
+        The output of <literal>?c</literal> is not influenced by
+        <literal>locale</literal>, <literal>number_format</literal>,
+        etc.</para>
+
+        <para>The <literal>c</literal> built-in will format string values to
+        string literals. Like if the <literal>c_format</literal> setting is
+        <quote>JSON</quote>, then <literal>{"fullName":
+        ${fullName?c}}</literal> will output something like
+        <literal>{"fullName": "John Doe"}</literal>, where the quotation marks
+        (and <literal>\</literal> escaping if needed) were added by
+        <literal>?c</literal>.</para>
+
+        <note>
+          <para>At least as of 2.3.32, the <literal>c</literal> built-in
+          doesn't support date/time/datetime values, only numbers, booleans,
+          and strings.</para>
+        </note>
+
+        <para>When formatting for computers, at some places you want to output
+        a <literal>null</literal> (or its equivalent in the target language)
+        if the value you print is missing/<literal>null</literal>. The
+        convenient shortcut for that is using the <literal>cn</literal>
+        built-in (the <quote>n</quote> stands for Nullable), as in
+        <literal>${<replaceable>someValue</replaceable>?cn}</literal>. It's
+        behaves like the <literal>c</literal> built-in, except that if
+        <literal><replaceable>someValue</replaceable></literal> evaluates to
+        <literal>null</literal>/missing, it outputs the
+        <literal>null</literal> literal in the syntax that the
+        <literal>c_format</literal> setting specifies, instead of stopping
+        with missing value error.</para>
+
+        <para>For the templates where the output is obviously only for
+        computer consumption, some prefers setting the
+        <literal>number_format</literal>, and
+        <literal>boolean_format</literal> settings to <literal>"c"</literal>.
+        (Before 2.3.32, for <literal>number_format</literal> you have to use
+        <literal>"computer"</literal> instead.) Then
+        <literal>${<replaceable>someValue</replaceable>}</literal>, for
+        number, and boolean values will format like <literal>?c</literal>
+        does. To output string literals (i.e, to add quotation and escaping),
+        you will still need an explicit <literal>?c</literal>. Also if you
+        need to output <literal>null</literal> literals, you still have to use
+        <literal>?cn</literal>.</para>
+
+        <para>These are the table of supported <literal>c_format</literal>
+        setting values (as of FreeMarker 2.3.32), and their intended
+        use:</para>
+
+        <itemizedlist>
+          <listitem>
+            <para><literal>JSON</literal>: JSON generation</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>JavaScript</literal>: JavaScript generation</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>Java</literal>: Java source code generation</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>XS</literal>: XML Schema compliant XML
+            generation</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>default 2.3.0</literal>: Default for backward
+            compatibility if the <link
+            linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
+            setting</link> is less than 2.3.21. Avoid!</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>default 2.3.21</literal>: Default for backward
+            compatibility if the <link
+            linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
+            setting</link> is equal or greater than 2.3.21. Avoid!</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>The behaviour of these is documented at the <literal>c</literal>
+        built-in: <link linkend="ref_builtin_c">for numbers</link>, <link
+        linkend="ref_builtin_c_boolean">for boolean</link>, <link
+        linkend="ref_builtin_c_string">for strings</link>, <link
+        linkend="ref_builtin_cn">for null-s</link>.</para>
+      </section>
+
       <section xml:id="dgui_misc_whitespace">
         <title>White-space handling</title>
 
@@ -12705,7 +12815,8 @@ grant codeBase "file:/path/to/freemarker.jar"
 
           <listitem>
             <para>c <link linkend="ref_builtin_c">for strings</link>, <link
-            linkend="ref_builtin_c_boolean">for booleans</link></para>
+            linkend="ref_builtin_c_boolean">for booleans</link>, <link
+            linkend="ref_builtin_c_string">for strings</link></para>
           </listitem>
 
           <listitem>
@@ -12745,6 +12856,10 @@ grant codeBase "file:/path/to/freemarker.jar"
             <para><link linkend="ref_builtin_chunk">chunk</link></para>
           </listitem>
 
+          <listitem>
+            <para><link linkend="ref_builtin_cn">cn</link></para>
+          </listitem>
+
           <listitem>
             <para><link linkend="ref_builtin_contains">contains</link></para>
           </listitem>
@@ -13322,6 +13437,115 @@ GreEN mouse
           <literal>locale</literal> (language, country).</para>
         </section>
 
+        <section xml:id="ref_builtin_c_string">
+          <title>c (for string value)</title>
+
+          <indexterm>
+            <primary>c built-in</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>format</primary>
+
+            <secondary>string</secondary>
+          </indexterm>
+
+          <note>
+            <para>The <literal>c</literal> built-in also works <link
+            linkend="ref_builtin_c">on numbers</link>, and <link
+            linkend="ref_builtin_c_boolean">on booleans</link>!</para>
+          </note>
+
+          <note>
+            <para>To provide a background, see <xref
+            linkend="dgui_misc_computer_vs_human_format"/></para>
+          </note>
+
+          <note>
+            <para>The <literal>c</literal> built-in supports strings since
+            FreeMarker 2.3.32.</para>
+          </note>
+
+          <para>This built-in converts a string to a <quote>computer
+          language</quote> literal, according the value of the <link
+          linkend="gloss.c_format"><literal>c_format</literal>
+          setting</link>.</para>
+
+          <para>For the <literal>c_format</literal>-s that are built into
+          FreeMarker the rules are the following:</para>
+
+          <itemizedlist>
+            <listitem>
+              <para><quote>JSON</quote>, <quote>default 2.3.0</quote>, and
+              <quote>default 2.3.21</quote>: Gives a JSON string literal, that
+              is, it will be surrounded with quotation marks
+              (<literal>"</literal>), and will be escaped using backslash
+              (<literal>\</literal>) where needed. For the exact escaping
+              rules see <link linkend="ref_builtin_json_string">the
+              <literal>json_string</literal> built-in</link>.</para>
+            </listitem>
+
+            <listitem>
+              <para><quote>JavaScript</quote>: Almost the same as JSON, but
+              uses <literal>\x<replaceable>XX</replaceable></literal> instead
+              of <literal>\u<replaceable>XXXX</replaceable></literal> where
+              possible. For the exact escaping rules see <link
+              linkend="ref_builtin_js_string">the <literal>js_string</literal>
+              built-in</link>, except that this won't escape the apostrophe
+              quote (since it knows that it has used quotation marks around
+              the string literal).</para>
+            </listitem>
+
+            <listitem>
+              <para><quote>Java</quote>: Gives a Java string literal, that is,
+              it will be surrounded with quotation marks
+              (<literal>"</literal>), and will be escaped using backslash
+              (<literal>\</literal>) where needed. For the exact escaping
+              rules see <link linkend="ref_builtin_j_string">the
+              <literal>j_string</literal> built-in</link>.</para>
+            </listitem>
+
+            <listitem>
+              <para><quote>XS</quote>: Leaves the string as is. The idea is
+              that you will insert the value as the body of an XML element, or
+              into an XML attribute, so it needs no quotation, or any other
+              special syntax. While it does need XML encoding, that should be
+              handled by the <link linkend="dgui_misc_autoescaping">automatic
+              escaping facility</link>, and not by the
+              <literal>c_format</literal> facility.</para>
+            </listitem>
+          </itemizedlist>
+
+          <para>If the value the <literal>c</literal> built-in is applied on
+          is <literal>null</literal>/missing, it will stop the template
+          processing with error, just like most other built-ins. If instead
+          you want to output a <literal>null</literal> literal, see the <link
+          linkend="ref_builtin_cn_string"><literal>cn</literal>
+          built-in</link>.</para>
+        </section>
+
+        <section xml:id="ref_builtin_cn_string">
+          <title>cn (for string value)</title>
+
+          <indexterm>
+            <primary>cn built-in</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>format</primary>
+
+            <secondary>string</secondary>
+          </indexterm>
+
+          <para>This does the same as the <link
+          linkend="ref_builtin_c_string"><literal>c</literal> built-in</link>,
+          but when applied on a <literal>null</literal>/missing value, it will
+          output a <literal>null</literal> value according the <link
+          linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+          See more details about formatting a <literal>null</literal> <link
+          linkend="ref_builtin_cn">here</link>.</para>
+        </section>
+
         <section xml:id="ref_builtin_c_lower_case">
           <title>c_lower_case</title>
 
@@ -13944,10 +14168,11 @@ String BEAN_NAME = "${beanName?j_string}";</programlisting>
           JSON strings must be quoted with <literal>"</literal>.</para>
 
           <para>The escaping rules are almost identical to those <link
-          linkend="ref_builtin_j_string">documented for
+          linkend="ref_builtin_js_string">documented for
           <literal>js_string</literal></link>. The differences are that
-          <literal>'</literal> is not escaped at all, that &gt; is escaped as
-          \u003E (not as \&gt;), and that
+          <literal>'</literal> is not escaped at all, that
+          <literal>&gt;</literal> is escaped as <literal>\u003E</literal> (not
+          as <literal>\&gt;</literal>), and that
           <literal>\u<replaceable>XXXX</replaceable></literal> escapes are
           used instead of <literal>\x<replaceable>XX</replaceable></literal>
           escapes.</para>
@@ -15700,7 +15925,7 @@ rif: foo XYr baar</programlisting>
         </section>
 
         <section xml:id="ref_builtin_c">
-          <title>c (when used with numerical value)</title>
+          <title>c (for numbers)</title>
 
           <indexterm>
             <primary>c built-in</primary>
@@ -15721,69 +15946,141 @@ rif: foo XYr baar</programlisting>
           </indexterm>
 
           <note>
-            <para>This built-in exists since FreeMarker 2.3.3.</para>
+            <para>The <literal>c</literal> built-in also works <link
+            linkend="ref_builtin_c_boolean">on booleans</link>, and <link
+            linkend="ref_builtin_c_string">on strings</link>!</para>
           </note>
 
-          <para>This built-in converts a number to string for a
-          <quote>computer language</quote> as opposed to for human audience.
-          That is, it formats with the rules that programming languages used
-          to use, which is independent of all the locale and number format
-          settings of FreeMarker. It always uses dot as decimal separator, and
-          it never uses grouping separators (like in 3,000,000), nor
-          <quote>+</quote> sign (like +1), nor superfluous leading or trailing
-          0-s (like 03 or 1.0). It usually avoids exponential form (like it
-          will never format 10000000000 as 1E10), but see the details below
-          regarding that.</para>
-
-          <para>This built-in is crucial because be default (like with
+          <note>
+            <para>To provide a background, see <xref
+            linkend="dgui_misc_computer_vs_human_format"/></para>
+          </note>
+
+          <para>This built-in converts a number to a <quote>computer
+          language</quote> literal, as opposed to format it for human reading.
+          This format is independent of the <literal>locale</literal> (human
+          language, country) and <literal>number_format</literal> settings of
+          FreeMarker. Instead, it depends on the <link
+          linkend="gloss.c_format"><literal>c_format</literal> setting</link>,
+          which is usually something like <literal>"JSON"</literal>; a
+          computer language.</para>
+
+          <para>This built-in is crucial because by default (like with
           <literal>${x}</literal>) numbers are converted to strings with the
-          locale (language, country) specific number formatting, which is for
-          human readers (like 3000000 is possibly printed as 3,000,000). When
-          the number is printed not for human audience (e.g., for a database
-          record ID used as the part of an URL, or as invisible field value in
-          a HTML form, or for printing CSS/JavaScript numerical literals) you
-          must use this built-in to format the number (i.e., use
+          locale specific number formatting, so 3000000 is possibly printed as
+          <literal>3,000,000</literal> (i.e., with grouping separators), or
+          <literal>3.14</literal> is possibly printed as
+          <literal>3,14</literal> (i.e., with a different decimal separator).
+          When the number is printed not for human audience (e.g., for a
+          database record ID used as the part of an URL, or as invisible field
+          value in a HTML form, or for printing CSS/JavaScript numerical
+          literals) you must use this built-in to format the number (i.e., use
           <literal>${x?c}</literal> instead of <literal>${x}</literal>), or
-          else the output will be possibly broken depending on the current
-          number formatting settings and locale (like the decimal point is not
-          dot, but comma in many countries) and the value of the number (like
-          big numbers are possibly <quote>damaged</quote> by grouping
-          separators).</para>
+          else the output will be possibly unparsable for the consumer.</para>
 
-          <para>Further details:</para>
+          <para>The exact format of numbers depend on value of the
+          <literal>c_format</literal> setting, but for all the
+          <literal>c_format</literal>-s that are built into FreeMarker, these
+          sand:</para>
 
           <itemizedlist>
             <listitem>
-              <para>Rounding: The conversion is only guaranteed to be lossless
-              if the <link
-              linkend="pgui_config_incompatible_improvements"><literal>incompatible_improvements</literal>
-              FreeMarker configuration setting</link> is set to 2.3.32 or
-              higher. Before that, the format never uses exponential form, and
-              is limited to 16 digits after the decimal dot, so rounding can
-              occur (e.g. 1E-17 will be shown as 0).</para>
-            </listitem>
-
-            <listitem>
-              <para>Special floating point values: If the
-              <literal>incompatible_improvements</literal> FreeMarker
-              configuration setting is set to 2.3.24 or higher (also if it's
-              set to 2.3.20 or higher and you are outside a string literal),
-              this built-in will return <literal>"INF"</literal>,
-              <literal>"-INF"</literal> and <literal>"NaN"</literal> for
-              positive/negative infinity and IEEE floating point Not-a-Number,
-              respectively. These are the XML Schema compatible
-              representations of these special values. With too low
-              <literal>incompatible_improvements</literal> it returns what
-              <literal>java.text.DecimalFormat</literal> did with US locale,
-              none of which is understood by any (common) computer
-              language.</para>
+              <para>It always uses dot as decimal separator</para>
+            </listitem>
+
+            <listitem>
+              <para>It always uses dot as decimal separator</para>
+            </listitem>
+
+            <listitem>
+              <para>Never uses <quote>+</quote> sign (as in +1), except maybe
+              after the <quote>E</quote> that signifies the exponent
+              part</para>
+            </listitem>
+
+            <listitem>
+              <para>No superfluous leading or trailing 0-s (like 03 or
+              1.0)</para>
+            </listitem>
+
+            <listitem>
+              <para>It usually avoids exponential form (like it will never
+              format 10000000000 as 1E10), but see the details below regarding
+              that.</para>
+            </listitem>
+          </itemizedlist>
+
+          <para>Finder details:</para>
+
+          <itemizedlist>
+            <listitem>
+              <para>Rounding:</para>
+
+              <itemizedlist>
+                <listitem>
+                  <para>For all non-deprecated <literal>c_format</literal>-s
+                  that are built into FreeMarker: There's no rounding.</para>
+                </listitem>
+
+                <listitem>
+                  <para>For the deprecated <literal>c_format</literal>-s,
+                  <quote>default 2.3.0</quote>, and <quote>default
+                  2.3.21</quote>: The numbers are limited to 16 digits after
+                  the decimal dot, so rounding can occur. The these formats
+                  never use exponential form either, so the decimal point
+                  place is fixed. Thus, for example, 1E-17 will be formatted
+                  as <literal>0</literal>. </para>
+                </listitem>
+              </itemizedlist>
             </listitem>
 
             <listitem>
-              <para>Exponential form: Before
-              <literal>incompatible_improvements</literal> 2.3.32, it was
-              never used. After that, it's uses exponential form in these
-              cases:</para>
+              <para>Special floating point values, positive infinity, negative
+              infinity, and NaN (for Not-a-Number):</para>
+
+              <itemizedlist>
+                <listitem>
+                  <para>For <literal>c_format</literal>-s <quote>JSON</quote>,
+                  and <quote>JavaScript</quote>: <literal>Infinity</literal>,
+                  <literal>-Infinity</literal>, <literal>NaN</literal></para>
+                </listitem>
+
+                <listitem>
+                  <para>For <literal>c_format</literal> <quote>Java</quote>:
+                  If the value has Java type <literal>double</literal> or
+                  <literal>Double</literal>:
+                  <literal>Double.POSITIVE_INFINITY</literal>,
+                  <literal>Double.NEGATIVE_INFINITY</literal>,
+                  <literal>Double.NaN</literal>. If the value has Java type
+                  <literal>float</literal> or <literal>Float</literal>:
+                  <literal>Float.POSITIVE_INFINITY</literal>,
+                  <literal>Float.NEGATIVE_INFINITY</literal>,
+                  <literal>Float.NaN</literal>.</para>
+                </listitem>
+
+                <listitem>
+                  <para>For <literal>c_format</literal> <quote>XS</quote>, and
+                  also for the deprecated <literal>c_format</literal>,
+                  <quote>default 2.3.21</quote>: <literal>INF</literal>,
+                  <literal>-INF</literal>, and <literal>NaN</literal>.</para>
+                </listitem>
+
+                <listitem>
+                  <para>For the deprecated <literal>c_format</literal>,
+                  <quote>default 2.3.0</quote>: Gives what
+                  <literal>java.text.DecimalFormat</literal> does with US
+                  locale, which are <literal>∞</literal>,
+                  <literal>-∞</literal>, and <literal>�</literal> (U+FFFD,
+                  replacement character).</para>
+                </listitem>
+              </itemizedlist>
+            </listitem>
+
+            <listitem>
+              <para>Exponential form is used by all non-deprecated
+              <literal>c_format</literal>-s that are built into FreeMarker
+              (but not by the deprecated <quote>default 2.3.0</quote>, and
+              <quote>default 2.3.21</quote>):</para>
 
               <itemizedlist>
                 <listitem>
@@ -15828,26 +16125,128 @@ rif: foo XYr baar</programlisting>
 
                   <para>Note that by default FreeMarker doesn't use
                   <literal>double</literal> or <literal>float</literal>, so
-                  such values are likely can only come from the
-                  data-model.</para>
+                  such values are likely come from the data-model.</para>
                 </listitem>
               </itemizedlist>
             </listitem>
 
             <listitem>
-              <para>The output never contains superfluous zeros after the
-              decimal point. Thus, unlike in Java, you can't tell apart a
-              <literal>double</literal> from an <literal>int</literal>, if
-              they store the same number mathematically, because the output
-              will look the same for both. (This is because the template
-              language only have a single number type, so you don't have a
-              good control over the backing Java type.)</para>
+              <para>Currently, in the <literal>c_format</literal>-s that are
+              built into FreeMarker, the output never contains superfluous
+              zeros after the decimal point. Thus, unlike in Java, you can't
+              tell apart a <literal>double</literal> from an
+              <literal>int</literal>, if they store the same number
+              mathematically, because the output will look the same for both.
+              (This is because the template language only have a single number
+              type, so you don't have a good control over the backing Java
+              type.)</para>
             </listitem>
           </itemizedlist>
 
-          <para>Note that the <literal>c</literal> built-in <link
-          linkend="ref_builtin_c_boolean">also works on
-          booleans</link>.</para>
+          <para>If you only generate output that's computer language and isn't
+          read by end-users, you may prefer to set the
+          <literal>number_format</literal> configuration setting to
+          <literal>"c"</literal> (since FreeMarker 2.3.32,
+          <literal>"computer"</literal> before that), in which case
+          <literal>${<replaceable>aNumber</replaceable>}</literal> will have
+          the same output as
+          <literal>${<replaceable>aNumber</replaceable>?c}</literal>. (In this
+          case you should use a <literal>c_format</literal> like
+          <literal>"JSON"</literal>, and not some of the strictly backward
+          compatible defaults, as those are emulating some confusing old
+          glitches.)</para>
+
+          <para>If the value the <literal>c</literal> built-in is applied on
+          is <literal>null</literal>/missing, it will stop the template
+          processing with error, just like most other built-ins. If instead
+          you want to output a <literal>null</literal> literal, see the <link
+          linkend="ref_builtin_cn"><literal>cn</literal>
+          built-in</link>.</para>
+        </section>
+
+        <section xml:id="ref_builtin_cn">
+          <title>cn (for numbers)</title>
+
+          <indexterm>
+            <primary>cn built-in</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>type-casting</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>converting between types</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>format</primary>
+
+            <secondary>number</secondary>
+          </indexterm>
+
+          <note>
+            <para><literal>cn</literal> works with all types that
+            <literal>c</literal> does, and thus for strings and booleans as
+            well. The formatting of <literal>null</literal>/missing doesn't
+            depend on the type of course (as we have no value that could have
+            a type).</para>
+          </note>
+
+          <note>
+            <para>See <xref linkend="dgui_misc_computer_vs_human_format"/> for
+            background</para>
+          </note>
+
+          <note>
+            <para>This built-in exists since FreeMarker 2.3.32</para>
+          </note>
+
+          <para>This is the same as the <link
+          linkend="ref_builtin_c"><literal>c</literal> built-in</link>, except
+          if the value on its left side is <literal>null</literal>/missing,
+          this won't stop with error, but outputs a <literal>null</literal>
+          literal that's appropriate for the current
+          <literal>c_format</literal> setting:</para>
+
+          <itemizedlist>
+            <listitem>
+              <para>For <quote>JSON</quote>, <quote>Java</quote>,
+              <quote>JavaScript</quote>, <quote>default 2.3.0</quote>,
+              <quote>default 2.3.21</quote>: <literal>null</literal></para>
+            </listitem>
+
+            <listitem>
+              <para>For <quote>XS</quote> (used for generating XML that
+              follows XML Schema principles): 0 length string (i.e.,
+              <literal>${<replaceable>thisIsNull</replaceable>?cn}</literal>
+              prints nothing), which is often not good enough (see below), but
+              we can't do better with a
+              <literal><replaceable>stringValue</replaceable>?c</literal>
+              alone. The idea is that you write something like
+              <literal>&lt;full-name&gt;${fullName?nc}&lt;/full-name&gt;</literal>,
+              or <literal>&lt;user <replaceable>...</replaceable>
+              full-name="${fullName?nc}" /&gt;</literal>, and then, in case
+              <literal>fullName</literal> is <literal>null</literal>, the
+              output will be
+              <literal>&lt;full-name&gt;&lt;/full-name&gt;</literal>, and
+              <literal>&lt;user <replaceable>...</replaceable> full-name=""
+              /&gt;</literal>. Some applications accept that as the equivalent
+              of a <literal>null</literal>, at least where a string value is
+              expected according the XML Schema.</para>
+
+              <para>Note that the XML Schema approach is that you skip
+              outputting the whole <literal>full-name</literal> XML element,
+              or XML attribute. For that you have to write <literal>&lt;#if
+              fullName??&gt;&lt;full-name&gt;${full-name?c}&lt;/full-name&gt;&lt;/#if&gt;</literal>,
+              and <literal>&lt;user <replaceable>...</replaceable> &lt;#if
+              fullName??&gt;full-name="${fullName?c}"&lt;/#if&gt;
+              /&gt;</literal>. When using such condition, and the value is a
+              string (as with this example), you might as well just write
+              <literal>${fullName}</literal>, without the
+              <literal>?c</literal>.</para>
+            </listitem>
+          </itemizedlist>
         </section>
 
         <section xml:id="ref_builtin_is_infinite">
@@ -16991,7 +17390,7 @@ Tue, Apr 8, '03
         </indexterm>
 
         <section xml:id="ref_builtin_c_boolean">
-          <title>c (when used with boolean)</title>
+          <title>c (for boolean value)</title>
 
           <indexterm>
             <primary>c built-in</primary>
@@ -17012,28 +17411,73 @@ Tue, Apr 8, '03
           </indexterm>
 
           <note>
-            <para>This built-in exists since FreeMarker 2.3.20.</para>
+            <para>The <literal>c</literal> built-in also works <link
+            linkend="ref_builtin_c_boolean"> <link linkend="ref_builtin_c">on
+            numbers</link></link>, and <link linkend="ref_builtin_c_string">on
+            strings</link>!</para>
+          </note>
+
+          <note>
+            <para>To provide a background, see <xref
+            linkend="dgui_misc_computer_vs_human_format"/></para>
           </note>
 
-          <para>This built-in converts a boolean to string for a
-          <quote>computer language</quote> as opposed to for human audience.
-          The result will be <literal>"true"</literal> or
-          <literal>"false"</literal>, regardless of the
-          <literal>boolean_format</literal> configuration setting, as that
-          setting is meant to specify the format for human consumption. When
-          generating boolean literals for JavaScript and such, this should be
-          used.</para>
+          <note>
+            <para>The <literal>c</literal> built-in supports booleans since
+            FreeMarker 2.3.20.</para>
+          </note>
+
+          <para>This built-in converts a boolean to a <quote>computer
+          language</quote> literal, as opposed to format it for human reading.
+          This formats is independent of the <literal>boolean_format</literal>
+          configuration setting, as that setting is meant to specify the
+          format for human readers. Instead, it depends on the <link
+          linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+          However, currently all <literal>c_format</literal> that's built into
+          FreeMarker result will give <literal>true</literal> or
+          <literal>false</literal>.</para>
+
+          <para>When outputting boolean literals for JavaScript, JSON, Java,
+          and many other languages, always use this built-in, instead of
+          relying on <literal>boolean_format</literal>.</para>
 
           <para>If you only generate output that's computer language and isn't
-          read by end-users, you may want to set the
+          read by end-users, you may prefer to set the
           <literal>boolean_format</literal> configuration setting to
           <literal>c</literal> (since FreeMarker 2.3.29), in which case
           <literal>${<replaceable>aBoolean</replaceable>}</literal> will have
           the same output as
           <literal>${<replaceable>aBoolean</replaceable>?c}</literal>.</para>
 
-          <para>Note that this built-in <link linkend="ref_builtin_c">also
-          works on strings</link>.</para>
+          <para>If the value the <literal>c</literal> built-in is applied on
+          is <literal>null</literal>/missing, it will stop the template
+          processing with error, just like most other built-ins. If instead
+          you want to output a <literal>null</literal> literal, see the <link
+          linkend="ref_builtin_cn_string"><literal>cn</literal>
+          built-in</link>.</para>
+        </section>
+
+        <section xml:id="ref_builtin_cn_boolean">
+          <title>cn (for boolean value)</title>
+
+          <indexterm>
+            <primary>cn built-in</primary>
+          </indexterm>
+
+          <indexterm>
+            <primary>format</primary>
+
+            <secondary>boolean</secondary>
+          </indexterm>
+
+          <para>This does the same as the <link
+          linkend="ref_builtin_c_boolean"><literal>c</literal>
+          built-in</link>, but when applied on a
+          <literal>null</literal>/missing value, it will output a
+          <literal>null</literal> value according the <link
+          linkend="gloss.c_format"><literal>c_format</literal> setting</link>.
+          See more details about formatting a <literal>null</literal> <link
+          linkend="ref_builtin_cn">here</link>.</para>
         </section>
 
         <section xml:id="ref_builtin_string_for_boolean">
@@ -17212,10 +17656,6 @@ N
             <primary>columnar printing of sequences</primary>
           </indexterm>
 
-          <note>
-            <para>This built-in exists since FreeMarker 2.3.3.</para>
-          </note>
-
           <para>This built-in splits a sequence into multiple sequences of the
           size given with the 1st parameter to the built-in (like
           <literal>mySeq?chunk(3)</literal>). The result is the sequence of
@@ -23886,9 +24326,7 @@ ${"'{}"}
         <section>
           <title>Synopsis</title>
 
-          <programlisting role="metaTemplate">
-<literal>&lt;#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>&gt;</literal>
-</programlisting>
+          <programlisting role="metaTemplate"><literal>&lt;#setting <replaceable>name</replaceable>=<replaceable>value</replaceable>&gt;</literal></programlisting>
 
           <para>Where:</para>
 
@@ -24318,6 +24756,18 @@ ${"'{}"}
               <literal>Configurable.setSQLDateAndTimeTimeZone(TimeZone)</literal>.</phrase></para>
             </listitem>
 
+            <listitem>
+              <para><indexterm>
+                  <primary>c_format</primary>
+                </indexterm><literal>c_format</literal> (since FreeMarker
+              2.3.32): Sets what format to use when formatting for computer
+              consumption, like <literal>"JSON"</literal>. Mostly prominently
+              this affects the <link
+              linkend="ref_builtin_c"><literal>c</literal> built-in</link>,
+              hence the name. See valid values and their meaning here: <xref
+              linkend="dgui_misc_computer_vs_human_format"/>.</para>
+            </listitem>
+
             <listitem>
               <para><indexterm>
                   <primary>url_escaping_charset</primary>
@@ -29584,6 +30034,129 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
           <title>Changes on the FTL side</title>
 
           <itemizedlist>
+            <listitem>
+              <para>Improved outputting values for computer/parser consumption
+              (such as for generating JSON, JavaScript values, values encoded
+              into URL-s):</para>
+
+              <itemizedlist>
+                <listitem>
+                  <para>Added new configuration setting, <link
+                  linkend="gloss.c_format"><literal>c_format</literal></link>
+                  (also settable via the <link
+                  linkend="ref.directive.setting"><literal>setting</literal>
+                  directive</link>). This specifies what format to use when
+                  formatting for computer consumption, like
+                  <literal>"JSON"</literal>. Most prominently, this affects
+                  the <link linkend="ref_builtin_c"><literal>c</literal>
+                  built-in</link>, hence the name. See valid setting values,
+                  and their meaning here: <xref
+                  linkend="dgui_misc_computer_vs_human_format"/>.</para>
+
+                  <para>If you set the <link
+                  linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
+                  setting</link> to 2.3.32, the default of
+                  <literal>c_format</literal> changes to
+                  <literal>JSON</literal>, which is a format that most targets
+                  can parse (because we just format simple values, not lists
+                  and maps). With lower
+                  <literal>incompatible_improvements</literal>, the default
+                  value is a <literal>c_format</literal> that emulates the old
+                  behavior of <literal>?c</literal> (where you can lose
+                  numerical prevision, etc.), so it's recommended to set it to
+                  something else.</para>
+                </listitem>
+
+                <listitem>
+                  <para><literal><link
+                  linkend="ref_builtin_c">?c</link></literal> now formats
+                  string values to string literals (with quotation marks and
+                  escaping), according the language specified in the <link
+                  linkend="gloss.c_format"><literal>c_format</literal>
+                  setting</link>, such as <literal>JSON</literal>,
+                  <literal>Java</literal>, etc. Earlier, <literal>?c</literal>
+                  only allowed numbers, and booleans.</para>
+
+                  <para>To generate JSON, you can now write a piece of
+                  template like this:</para>
+
+                  <programlisting role="template">"fullName": ${user.fullName?c},</programlisting>
+
+                  <para>Then the output will be like:</para>
+
+                  <programlisting role="output">"fullName": "John Doe",</programlisting>
+
+                  <para>Note that the quotation marks were added by
+                  <literal>?c</literal>, and weren't typed into the
+                  template.</para>
+                </listitem>
+
+                <listitem>
+                  <para>Added <link linkend="ref_builtin_cn">?cn</link>, which
+                  is like <literal><link
+                  linkend="ref_builtin_c">?c</link></literal>, except if the
+                  value is <literal>null</literal>/missing, it will output a
+                  <literal>null</literal> literal, according the language
+                  specified in the new <link
+                  linkend="gloss.c_format"><literal>c_format</literal>
+                  setting</link>.</para>
+
+                  <para>Let's say, in the previous example
+                  <literal>user.fullName</literal> is expected to be
+                  <literal>null</literal> sometimes. Then you can just use
+                  <literal>?cn</literal> instead if
+                  <literal>?c</literal>:</para>
+
+                  <programlisting role="template">"fullName": ${user.fullName?cn},</programlisting>
+
+                  <para>If said variable is <literal>null</literal>, the
+                  output will be like this (otherwise it will be a quoted
+                  string like earlier):</para>
+
+                  <programlisting role="output">"fullName": null,</programlisting>
+
+                  <para>Note with this approach you don't complicate the
+                  template anymore to avoid printing quotation marks. Of
+                  course, <literal>?cn</literal> works on numerical and
+                  boolean values as well.</para>
+                </listitem>
+
+                <listitem>
+                  <para><literal>c_format</literal>-s which are not merely for
+                  backward compatibility, the number formatting changes
+                  relatively to how <literal>?c</literal> did it in earlier
+                  versions. The change affects some non-whole numbers, and
+                  whole numbers with over 100 digits. The goal of this change
+                  is to make the formatting lossless, and also to avoiding
+                  huge output with exponents of high magnitude. See details at
+                  the documentation of the <link
+                  linkend="ref_builtin_c"><literal>c</literal>
+                  built-in</link>.</para>
+
+                  <para>Because setting the <link
+                  linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incompatible_improvements</literal>
+                  setting</link> to 2.3.32 will change the default of
+                  <literal>c_format</literal> to <literal>JSON</literal>, it
+                  will consequently also activate these changes. But this only
+                  affects number formatting done with <literal>?c</literal>,
+                  <literal>?cn</literal>, and with <quote>c</quote>
+                  <literal>number_format</literal>, and not number formatting
+                  in general.</para>
+                </listitem>
+
+                <listitem>
+                  <para>For consistency, when setting the
+                  <literal>number_format</literal> setting (also when
+                  formatting with
+                  <literal>?string(<replaceable>format</replaceable>)</literal>),
+                  now <quote>c</quote> can be used instead of
+                  <quote>computer</quote>. Both has the same effect on
+                  formatting, but <quote>c</quote> is preferred from now
+                  on.</para>
+                </listitem>
+              </itemizedlist>
+            </listitem>
+
             <listitem>
               <para><link
               xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-208">FREEMARKER-208</link>:
@@ -29601,29 +30174,6 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
               purposes), and not for humans.</para>
             </listitem>
 
-            <listitem>
-              <para>When setting the <literal>number_format</literal> setting
-              (also when formatting with
-              <literal>?string(<replaceable>format</replaceable>)</literal>),
-              now <quote>c</quote> can be used instead of
-              <quote>computer</quote>. The two mean the same, and
-              <quote>c</quote> is preferred from now on.</para>
-            </listitem>
-
-            <listitem>
-              <para>If <link
-              linkend="pgui_config_incompatible_improvements_how_to_set"><literal>incomplatible_improvements</literal></link>
-              is set to 2.3.32 (or higher), the behavior of
-              <literal>?c</literal> (and thus the <quote>computer</quote> and
-              <quote>c</quote> number format) has changed for some non-whole
-              numbers, and for whole numbers with over 100 digits. The goal of
-              this change is to make the formatting lossless, and also to
-              avoiding huge output with exponents of high magnitude. See
-              details at the documentation of the <link
-              linkend="ref_builtin_c"><literal>c</literal>
-              built-in</link>.</para>
-            </listitem>
-
             <listitem>
               <para>In <literal>freemarker.ext.xml</literal>, which is the
               old, long deprecated XML wrapper, that almost nobody uses
@@ -42584,6 +43134,36 @@ Apache Software Foundation.</programlisting>
       </glossdef>
     </glossentry>
 
+    <glossentry xml:id="gloss.c">
+      <glossterm>c</glossterm>
+
+      <glossdef>
+        <para>In the context of FreeMarker, this usually refers to the <link
+        linkend="ref_builtin_c"><literal>c</literal> built-in</link>, or
+        <quote>computer format</quote> in general.</para>
+      </glossdef>
+    </glossentry>
+
+    <glossentry xml:id="gloss.c_format">
+      <glossterm>c_format</glossterm>
+
+      <glossdef>
+        <para><literal>c_format</literal> (aka. <literal>cFormat</literal>,
+        <quote>c format</quote>) is a <link
+        linkend="pgui_config_settings">configuration setting</link> that
+        specifies how to format values for <quote>computer
+        consumption</quote>, that is, if the target is some computer language,
+        or other simpler parser, and not a human reader. The value of this
+        setting is usually a computer language, like <quote>JSON</quote>, or
+        <quote>Java</quote>. The name refers to the <link
+        linkend="ref_builtin_c"><literal>c</literal> built-in</link>, which is
+        the typical way of formatting simple values for such output (e.g.
+        <literal>&lt;a href="/product/${product.id?c}"&gt;</literal>). To
+        understand the topic more, see: <xref
+        linkend="dgui_misc_computer_vs_human_format"/></para>
+      </glossdef>
+    </glossentry>
+
     <glossentry xml:id="gloss.character">
       <glossterm>Character</glossterm>
 
diff --git a/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java b/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java
new file mode 100644
index 00000000..d8ca4d81
--- /dev/null
+++ b/src/test/java/freemarker/core/BooleanFormatEnvironmentCachingTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class BooleanFormatEnvironmentCachingTest extends TemplateTest {
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration conf = super.createConfiguration();
+        conf.setCFormat(CustomCFormat.INSTANCE);
+        conf.setBooleanFormat("c");
+        return conf;
+    }
+
+    @Test
+    public void test() throws TemplateException, IOException {
+        assertOutput(
+                ""
+                        + "${true} ${true} ${false} ${false} "
+                        + "<#setting cFormat='JSON'>${true} ${true} ${false} ${false} "
+                        + "<#setting booleanFormat='y,n'>${true} ${true} ${false} ${false} "
+                        + "<#setting cFormat='Java'>${true} ${true} ${false} ${false} "
+                        + "<#setting booleanFormat='c'>${true} ${true} ${false} ${false} ",
+                ""
+                        + "TRUE TRUE FALSE FALSE "
+                        + "true true false false "
+                        + "y y n n "
+                        + "y y n n "
+                        + "true true false false "
+                        + "");
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/freemarker/core/CAndCnBuiltInTest.java b/src/test/java/freemarker/core/CAndCnBuiltInTest.java
new file mode 100644
index 00000000..af7d4622
--- /dev/null
+++ b/src/test/java/freemarker/core/CAndCnBuiltInTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Timestamp;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.template.Version;
+import freemarker.template._VersionInts;
+import freemarker.test.TemplateTest;
+
+public class CAndCnBuiltInTest extends TemplateTest {
+
+    @Before
+    public void addModelVariables() {
+        addToDataModel("double1", 1.0);
+        addToDataModel("double2", 1.000000000000001);
+        addToDataModel("double3", 0.0000000000000001);
+        addToDataModel("double4", -0.0000000000000001);
+        addToDataModel("bigDecimal1", new BigDecimal("1"));
+        addToDataModel("bigDecimal2", new BigDecimal("0.0000000000000001"));
+        addToDataModel("doubleInf", Double.POSITIVE_INFINITY);
+        addToDataModel("doubleNegativeInf", Double.NEGATIVE_INFINITY);
+        addToDataModel("doubleNaN", Double.NaN);
+        addToDataModel("floatInf", Float.POSITIVE_INFINITY);
+        addToDataModel("floatNegativeInf", Float.NEGATIVE_INFINITY);
+        addToDataModel("floatNaN", Float.NaN);
+        addToDataModel("string", "a\nb");
+        addToDataModel("long", Long.MAX_VALUE);
+        addToDataModel("int", Integer.MAX_VALUE);
+        addToDataModel("bigInteger", new BigInteger("123456789123456789123456789123456789"));
+        addToDataModel("dateTime", new Timestamp(1671641049876L));
+        addToDataModel("booleanTrue", true);
+        addToDataModel("booleanFalse", false);
+    }
+
+    @Test
+    public void testCWithNumber() throws TemplateException, IOException {
+        testWithNumber("c");
+    }
+
+    @Test
+    public void testCnWithNumber() throws TemplateException, IOException {
+        testWithNumber("cn");
+    }
+
+    void testWithNumber(String builtInName) throws TemplateException, IOException {
+        testWithNumber(builtInName, Configuration.VERSION_2_3_20);
+        testWithNumber(builtInName, Configuration.VERSION_2_3_21);
+        testWithNumber(builtInName, Configuration.VERSION_2_3_31);
+        testWithNumber(builtInName, Configuration.VERSION_2_3_32);
+    }
+
+    void testWithNumber(String builtInName, Version ici) throws TemplateException, IOException {
+        // Always the same
+        assertOutput("${double1? " + builtInName + "}", "1");
+        assertOutput("${double2?" + builtInName + "}", "1.000000000000001");
+        assertOutput("${bigDecimal1? " + builtInName + "}", "1");
+        assertOutput("${int? " + builtInName + "}", String.valueOf(Integer.MAX_VALUE));
+        assertOutput("${long? " + builtInName + "}", String.valueOf(Long.MAX_VALUE));
+        assertOutput("${bigInteger? " + builtInName + "}", "123456789123456789123456789123456789");
+
+        getConfiguration().setIncompatibleImprovements(ici);
+
+        if (ici.intValue() >= _VersionInts.V_2_3_32) {
+            assertOutput("${double3?" + builtInName + "}", "1E-16");
+            assertOutput("${double4?" + builtInName + "}", "-1E-16");
+            assertOutput("${bigDecimal2?" + builtInName + "}", "1E-16");
+        } else {
+            assertOutput("${double3?" + builtInName + "}", "0.0000000000000001");
+            assertOutput("${double4?" + builtInName + "}", "-0.0000000000000001");
+            assertOutput("${bigDecimal2?" + builtInName + "}", "0.0000000000000001");
+        }
+
+        for (String type : new String[] {"float", "double"}) {
+            String expectedInf;
+            String expectedNaN;
+            if (ici.intValue() >= _VersionInts.V_2_3_32) {
+                expectedInf = "Infinity";
+                expectedNaN = "NaN";
+            } else if (ici.intValue() >= _VersionInts.V_2_3_21) {
+                expectedInf = "INF";
+                expectedNaN = "NaN";
+            } else {
+                expectedInf = "\u221E";
+                expectedNaN = "\uFFFD";
+            }
+
+            assertOutput("${" + type + "Inf?" + builtInName + "}", expectedInf);
+            assertOutput("${" + type + "NegativeInf?" + builtInName + "}", "-" + expectedInf);
+            assertOutput("${" + type + "NaN?" + builtInName + "}", expectedNaN);
+        }
+    }
+
+    @Test
+    public void testWithNonNumber() throws TemplateException, IOException {
+        for (Version ici : new Version[] {
+                Configuration.VERSION_2_3_0, Configuration.VERSION_2_3_31, Configuration.VERSION_2_3_32 }) {
+            testWithNonNumber("c", ici);
+            testWithNonNumber("cn", ici);
+        }
+    }
+
+    private void testWithNonNumber(String builtInName, Version ici) throws TemplateException, IOException {
+        assertOutput("${string?" + builtInName + "}", "\"a\\nb\"");
+        assertOutput("${booleanTrue?" + builtInName + "}", "true");
+        assertOutput("${booleanFalse?" + builtInName + "}", "false");
+        assertErrorContains("${dateTime?" + builtInName + "}",
+                "Expected a number, boolean, or string");
+    }
+
+    @Test
+    public void testWithNull() throws TemplateException, IOException {
+        assertOutput("${noSuchVar?cn}", "null");
+        assertErrorContains("${noSuchVar?c}", "null or missing");
+    }
+    
+}
diff --git a/src/test/java/freemarker/core/CFormatTemplateTest.java b/src/test/java/freemarker/core/CFormatTemplateTest.java
new file mode 100644
index 00000000..414a9011
--- /dev/null
+++ b/src/test/java/freemarker/core/CFormatTemplateTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class CFormatTemplateTest extends TemplateTest {
+
+    @Before
+    public void addDataModelVariables() {
+        addToDataModel("s", "a'b\"c\u0001");
+    }
+
+    @Test
+    public void testBooleanAndNullFormat() throws TemplateException, IOException {
+        getConfiguration().setCFormat(CustomCFormat.INSTANCE);
+        assertOutput(
+                ""
+                        + "${true?c} ${false?c} ${null?cn} "
+                        + "JSON: <#setting c_format='JSON'>${true?c} ${false?c} ${null?cn}",
+                ""
+                        + "TRUE FALSE NULL "
+                        + "JSON: true false null");
+        assertOutput(
+                "<#setting boolean_format='c'>"
+                        + "${true} ${false} "
+                        + "JSON: <#setting c_format='JSON'>${true} ${false}",
+                ""
+                        + "TRUE FALSE "
+                        + "JSON: true false");
+    }
+
+    @Test
+    public void testStringFormat() throws TemplateException, IOException {
+        assertOutput(
+                ""
+                        + "Default: ${s?c} "
+                        + "XS: <#setting c_format='XS'>${s?c} "
+                        + "JavaScript: <#setting c_format='JavaScript'>${s?c} "
+                        + "JSON: <#setting c_format='JSON'>${s?c} "
+                        + "Java: <#setting c_format='Java'>${s?c} ",
+                ""
+                        + "Default: \"a'b\\\"c\\u0001\" "
+                        + "XS: a'b\"c\u0001 "
+                        + "JavaScript: \"a'b\\\"c\\x01\" "
+                        + "JSON: \"a'b\\\"c\\u0001\" "
+                        + "Java: \"a'b\\\"c\\u0001\" ");
+    }
+
+}
diff --git a/src/test/java/freemarker/core/CTemplateNumberFormatTest.java b/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
index 3ae7ad75..d20be03b 100644
--- a/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
+++ b/src/test/java/freemarker/core/CTemplateNumberFormatTest.java
@@ -41,7 +41,8 @@ public class CTemplateNumberFormatTest {
         testFormat(10000000000000000d, "1E16");
         testFormat(12300000000000000d, "1.23E16");
         testFormat(Double.NaN, "NaN");
-        testFormat(Double.POSITIVE_INFINITY, "INF");
+        testFormat(Double.POSITIVE_INFINITY, "Infinity");
+        testFormat(Double.NEGATIVE_INFINITY, "-Infinity");
         testFormat(1.9E-6, "0.0000019");
         testFormat(9.5E-7, "9.5E-7");
         testFormat(9999999.5, "9999999.5");
@@ -58,7 +59,8 @@ public class CTemplateNumberFormatTest {
         testFormat(100000000f, "1E8");
         testFormat(123000000f, "1.23E8");
         testFormat(Float.NaN, "NaN");
-        testFormat(Float.POSITIVE_INFINITY, "INF");
+        testFormat(Float.POSITIVE_INFINITY, "Infinity");
+        testFormat(Float.NEGATIVE_INFINITY, "-Infinity");
         testFormat(1.9E-6f, "0.0000019");
         testFormat(9.5E-7f, "9.5E-7");
         testFormat(1000000.5f, "1000000.5");
@@ -100,11 +102,12 @@ public class CTemplateNumberFormatTest {
 
     private void testFormat(Number n, String expectedResult) throws TemplateModelException,
         TemplateValueFormatException {
-        String actualResult = (String) CTemplateNumberFormat.INSTANCE.format(new SimpleNumber(n));
+        TemplateNumberFormat cTemplateNumberFormat = JSONCFormat.INSTANCE.getTemplateNumberFormat();
+        String actualResult = (String) cTemplateNumberFormat.format(new SimpleNumber(n));
         assertFormatResult(n, actualResult, expectedResult);
         if (!actualResult.equals("NaN") && !actualResult.equals("0") && !actualResult.startsWith("-")) {
             Number negativeN = negate(n);
-            actualResult = (String) CTemplateNumberFormat.INSTANCE.format(new SimpleNumber(negativeN));
+            actualResult = (String) cTemplateNumberFormat.format(new SimpleNumber(negativeN));
             assertFormatResult(negativeN, actualResult, "-" + expectedResult);
         }
     }
diff --git a/src/test/java/freemarker/core/CustomCFormat.java b/src/test/java/freemarker/core/CustomCFormat.java
new file mode 100644
index 00000000..18573cb5
--- /dev/null
+++ b/src/test/java/freemarker/core/CustomCFormat.java
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+package freemarker.core;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.NumberFormat;
+
+import freemarker.template.TemplateException;
+import freemarker.template.utility.StringUtil;
+
+class CustomCFormat extends CFormat {
+    final static CustomCFormat INSTANCE = new CustomCFormat();
+
+    private CustomCFormat() {
+    }
+
+    private static final TemplateNumberFormat TEMPLATE_NUMBER_FORMAT = new CTemplateNumberFormat(
+            "M:INF", "M:NINF", "M:NaN",
+            "M:INF", "M:NINF", "M:NaN");
+
+    private static final DecimalFormat LEGACY_NUMBER_FORMAT_PROTOTYPE =
+            (DecimalFormat) Default230CFormat.INSTANCE.getLegacyNumberFormat().clone();
+
+    static {
+        DecimalFormatSymbols symbols = LEGACY_NUMBER_FORMAT_PROTOTYPE.getDecimalFormatSymbols();
+        symbols.setInfinity("Infinity");
+        symbols.setNaN("NaN");
+        LEGACY_NUMBER_FORMAT_PROTOTYPE.setDecimalFormatSymbols(symbols);
+    }
+
+    @Override
+    TemplateNumberFormat getTemplateNumberFormat() {
+        return TEMPLATE_NUMBER_FORMAT;
+    }
+
+    @Override
+    NumberFormat getLegacyNumberFormat() {
+        return (NumberFormat) LEGACY_NUMBER_FORMAT_PROTOTYPE.clone();
+    }
+
+    @Override
+    String formatString(String s, Environment env) throws TemplateException {
+        return StringUtil.ftlQuote(s);
+    }
+
+    @Override
+    String getTrueString() {
+        return "TRUE";
+    }
+
+    @Override
+    String getFalseString() {
+        return "FALSE";
+    }
+
+    @Override
+    String getNullString() {
+        return "NULL";
+    }
+
+    @Override
+    public String getName() {
+        return "custom";
+    }
+}
diff --git a/src/test/java/freemarker/core/NumberFormatTest.java b/src/test/java/freemarker/core/NumberFormatTest.java
index 184fc213..4d4ce237 100644
--- a/src/test/java/freemarker/core/NumberFormatTest.java
+++ b/src/test/java/freemarker/core/NumberFormatTest.java
@@ -46,7 +46,6 @@ import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.Version;
 import freemarker.test.TemplateTest;
-import net.jcip.annotations.Immutable;
 
 @SuppressWarnings("boxing")
 public class NumberFormatTest extends TemplateTest {
@@ -326,14 +325,16 @@ public class NumberFormatTest extends TemplateTest {
         for (Version ici : new Version[] {
                 Configuration.VERSION_2_3_20,
                 Configuration.VERSION_2_3_21, Configuration.VERSION_2_3_30,
-                Configuration.VERSION_2_3_31 } ) {
+                Configuration.VERSION_2_3_31,
+                Configuration.VERSION_2_3_32 } ) {
             cfg.setIncompatibleImprovements(ici);
 
             boolean cBuiltInBroken = ici.intValue() < Configuration.VERSION_2_3_21.intValue();
             boolean cNumberFormatBroken = ici.intValue() < Configuration.VERSION_2_3_31.intValue();
 
             String humanAudienceOutput = "\u221e -\u221e \ufffd";
-            String computerAudienceOutput = "INF -INF NaN";
+            String computerAudienceOutput = ici.intValue() < Configuration.VERSION_2_3_32.intValue()
+                    ? "INF -INF NaN" : "Infinity -Infinity NaN";
 
             assertOutput(
                     "${pInf?c} ${nInf?c} ${nan?c}",
diff --git a/src/test/java/freemarker/core/TemplateConfigurationTest.java b/src/test/java/freemarker/core/TemplateConfigurationTest.java
index e48c38aa..ef4be2b2 100644
--- a/src/test/java/freemarker/core/TemplateConfigurationTest.java
+++ b/src/test/java/freemarker/core/TemplateConfigurationTest.java
@@ -164,6 +164,7 @@ public class TemplateConfigurationTest {
         SETTING_ASSIGNMENTS.put("dateFormat", "yyyy-#DDD");
         SETTING_ASSIGNMENTS.put("dateTimeFormat", "yyyy-#DDD-@HH:mm");
         SETTING_ASSIGNMENTS.put("locale", NON_DEFAULT_LOCALE);
+        SETTING_ASSIGNMENTS.put("CFormat", JavaScriptCFormat.INSTANCE);
         SETTING_ASSIGNMENTS.put("logTemplateExceptions", false);
         SETTING_ASSIGNMENTS.put("wrapUncheckedExceptions", true);
         SETTING_ASSIGNMENTS.put("newBuiltinClassResolver", TemplateClassResolver.ALLOWS_NOTHING_RESOLVER);
diff --git a/src/test/java/freemarker/template/ConfigurationTest.java b/src/test/java/freemarker/template/ConfigurationTest.java
index a3adc5ba..7e11be74 100644
--- a/src/test/java/freemarker/template/ConfigurationTest.java
+++ b/src/test/java/freemarker/template/ConfigurationTest.java
@@ -54,12 +54,15 @@ import freemarker.cache.TemplateLookupResult;
 import freemarker.cache.TemplateLookupStrategy;
 import freemarker.cache.TemplateNameFormat;
 import freemarker.core.BaseNTemplateNumberFormatFactory;
+import freemarker.core.CFormat;
 import freemarker.core.CombinedMarkupOutputFormat;
 import freemarker.core.Configurable;
 import freemarker.core.Configurable.SettingValueAssignmentException;
 import freemarker.core.Configurable.UnknownSettingException;
 import freemarker.core.ConfigurableTest;
 import freemarker.core.CustomHTMLOutputFormat;
+import freemarker.core.Default230CFormat;
+import freemarker.core.Default2321CFormat;
 import freemarker.core.DefaultTruncateBuiltinAlgorithm;
 import freemarker.core.DummyOutputFormat;
 import freemarker.core.Environment;
@@ -67,6 +70,9 @@ import freemarker.core.EpochMillisDivTemplateDateFormatFactory;
 import freemarker.core.EpochMillisTemplateDateFormatFactory;
 import freemarker.core.HTMLOutputFormat;
 import freemarker.core.HexTemplateNumberFormatFactory;
+import freemarker.core.JSONCFormat;
+import freemarker.core.JavaCFormat;
+import freemarker.core.JavaScriptCFormat;
 import freemarker.core.MarkupOutputFormat;
 import freemarker.core.OptInTemplateClassResolver;
 import freemarker.core.OutputFormat;
@@ -79,6 +85,7 @@ import freemarker.core.UndefinedOutputFormat;
 import freemarker.core.UnregisteredOutputFormatException;
 import freemarker.core.XHTMLOutputFormat;
 import freemarker.core.XMLOutputFormat;
+import freemarker.core.XSCFormat;
 import freemarker.core._CoreStringUtils;
 import freemarker.ext.beans.BeansWrapperBuilder;
 import freemarker.ext.beans.LegacyDefaultMemberAccessPolicy;
@@ -96,7 +103,7 @@ public class ConfigurationTest extends TestCase {
     public ConfigurationTest(String name) {
         super(name);
     }
-    
+
     public void testIncompatibleImprovementsChangesDefaults() {
         Version newVersion = Configuration.VERSION_2_3_21;
         Version oldVersion = Configuration.VERSION_2_3_20;
@@ -180,6 +187,20 @@ public class ConfigurationTest extends TestCase {
         cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_27);
         assertTrue(((DefaultObjectWrapper) cfg.getObjectWrapper()).getTreatDefaultMethodsAsBeanMembers());
         assertFalse(((DefaultObjectWrapper) cfg.getObjectWrapper()).getPreferIndexedReadMethod());
+
+        cfg = new Configuration(Configuration.VERSION_2_3_0);
+        assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_20);
+        assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_21);
+        assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
+        assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_32);
+        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+        cfg.setCFormat(JSONCFormat.INSTANCE); // Same as default, but explicitly set now
+        cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_31);
+        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
     }
 
     private void assertUses2322ObjectWrapper(Configuration cfg) {
@@ -286,6 +307,14 @@ public class ConfigurationTest extends TestCase {
             assertFalse(cfg.isCacheStorageExplicitlySet());
             assertTrue(cfg.getCacheStorage() instanceof SoftCacheStorage);
         }
+
+        assertFalse(cfg.isCFormatExplicitlySet());
+        //
+        cfg.setCFormat(XSCFormat.INSTANCE);
+        assertTrue(cfg.isCFormatExplicitlySet());
+        //
+        cfg.unsetCFormat();
+        assertFalse(cfg.isTemplateLookupStrategyExplicitlySet());
     }
     
     public void testTemplateLoadingErrors() throws Exception {
@@ -1888,7 +1917,32 @@ public class ConfigurationTest extends TestCase {
         }
     }
 
-    @Test
+    public void testCFormat() throws TemplateException {
+        Configuration cfg = new Configuration(Configuration.VERSION_2_3_21);
+
+        assertSame(Default2321CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, Default230CFormat.NAME);
+        assertSame(Default230CFormat.INSTANCE, cfg.getCFormat());
+        cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, JSONCFormat.NAME);
+        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+
+        cfg.setSetting(Configuration.C_FORMAT_KEY_CAMEL_CASE, "default");
+        cfg.setSetting(Configuration.C_FORMAT_KEY_SNAKE_CASE, Default2321CFormat.NAME);
+
+        for (CFormat standardCFormat : new CFormat[] {
+                        Default230CFormat.INSTANCE, Default2321CFormat.INSTANCE,
+                        JSONCFormat.INSTANCE, JavaScriptCFormat.INSTANCE, JavaCFormat.INSTANCE,
+                        XSCFormat.INSTANCE
+                }) {
+            cfg.setSetting(Configuration.C_FORMAT_KEY, standardCFormat.getName());
+            assertSame(standardCFormat, cfg.getCFormat());
+        }
+
+        // Object Builder value:
+        cfg.setSetting(Configuration.C_FORMAT_KEY, JSONCFormat.class.getName() + "()");
+        assertSame(JSONCFormat.INSTANCE, cfg.getCFormat());
+    }
+
     public void testFallbackOnNullLoopVariable() throws TemplateException {
         Configuration cfg = new Configuration(Configuration.VERSION_2_3_29);
         assertTrue(cfg.getFallbackOnNullLoopVariable());
diff --git a/src/test/java/freemarker/template/utility/StringUtilTest.java b/src/test/java/freemarker/template/utility/StringUtilTest.java
index 557c5dc4..0bb29345 100644
--- a/src/test/java/freemarker/template/utility/StringUtilTest.java
+++ b/src/test/java/freemarker/template/utility/StringUtilTest.java
@@ -34,7 +34,7 @@ import freemarker.core.ParseException;
 public class StringUtilTest {
 
     @Test
-    public void testV2319() {
+    public void testJavaScriptStringEncV2319() {
         assertEquals("\\n\\r\\f\\b\\t\\x00\\x19", StringUtil.javaScriptStringEnc("\n\r\f\b\t\u0000\u0019"));
     }
 
diff --git a/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl b/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
index 81501e54..9d4b61f6 100644
--- a/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
+++ b/src/test/resources/freemarker/test/templatesuite/templates/number-format.ftl
@@ -34,23 +34,29 @@ ${100000.5}
 ${100000.5}
 <#setting number_format = ",000.##">
 ${100000.5}
-<@assertEquals expected="1" actual=double?c />
-<@assertEquals expected="1.000000000000001" actual=double2?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=double3?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("-1E-16", "-0.0000000000000001") actual=double4?c />
-<@assertEquals expected="1" actual=bigDecimal?c />
-<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=bigDecimal2?c />
-<#if iciIntValue gte 2003021>
-  <@assertEquals expected="INF" actual="INF"?number?c />
-  <@assertEquals expected="INF" actual="INF"?number?c />
-  <@assertEquals expected="-INF" actual="-INF"?number?c />
-  <@assertEquals expected="-INF" actual="-INF"?number?float?c />
-  <@assertEquals expected="NaN" actual="NaN"?number?float?c />
-  <@assertEquals expected="NaN" actual="NaN"?number?float?c />
+<#setting number_format = "c">
+<@assertEquals expected="1" actual=double?string />
+<@assertEquals expected="1.000000000000001" actual=double2?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=double3?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("-1E-16", "-0.0000000000000001") actual=double4?string />
+<@assertEquals expected="1" actual=bigDecimal?string />
+<@assertEquals expected=(iciIntValue gte 2003032)?then("1E-16", "0.0000000000000001") actual=bigDecimal2?string />
+<#if iciIntValue gte 2003032>
+  <@assertEquals expected="Infinity" actual="INF"?number?string />
+  <@assertEquals expected="Infinity" actual="INF"?number?float?string />
+  <@assertEquals expected="-Infinity" actual="-INF"?number?string />
+  <@assertEquals expected="-Infinity" actual="-INF"?number?float?string />
+  <@assertEquals expected="NaN" actual="NaN"?number?string />
+  <@assertEquals expected="NaN" actual="NaN"?number?float?string />
+<#elseif iciIntValue == 2003031>
+  <@assertEquals expected="INF" actual="INF"?number?string />
+  <@assertEquals expected="INF" actual="INF"?number?float?string />
+  <@assertEquals expected="-INF" actual="-INF"?number?string />
+  <@assertEquals expected="-INF" actual="-INF"?number?float?string />
+  <@assertEquals expected="NaN" actual="NaN"?number?string />
+  <@assertEquals expected="NaN" actual="NaN"?number?float?string />
 <#else>
-  <#setting locale = "en_US">
-  <#setting number_format = "0.#">
-  <@assertEquals expected="INF"?number?string actual="INF"?number?c />
-  <@assertEquals expected="-INF"?number?string actual="-INF"?number?c />
-  <@assertEquals expected="NaN"?number?string actual="NaN"?number?c />
+  <@assertEquals expected="\x221E" actual="INF"?number?string />
+  <@assertEquals expected="-\x221E" actual="-INF"?number?string />
+  <@assertEquals expected="\xFFFD" actual="NaN"?number?string />
 </#if>
\ No newline at end of file