diff --git a/src/context.d.ts b/src/context.d.ts
index ce2c3568d8..06058f2d31 100644
--- a/src/context.d.ts
+++ b/src/context.d.ts
@@ -12,6 +12,9 @@ export interface Context {
   /** The current owner SVG element. */
   ownerSVGElement: SVGSVGElement;
 
+  /** The current locale. Defaults to "en-US". */
+  locale: string;
+
   /** The Plot’s (typically generated) class name, for custom styles. */
   className: string;
 
diff --git a/src/context.js b/src/context.js
index 3e3e55d705..1956e4114d 100644
--- a/src/context.js
+++ b/src/context.js
@@ -2,8 +2,8 @@ import {creator, select} from "d3";
 import {maybeClip} from "./options.js";
 
 export function createContext(options = {}) {
-  const {document = typeof window !== "undefined" ? window.document : undefined, clip} = options;
-  return {document, clip: maybeClip(clip)};
+  const {locale = "en-US", document = typeof window !== "undefined" ? window.document : undefined, clip} = options;
+  return {locale, document, clip: maybeClip(clip)};
 }
 
 export function create(name, {document}) {
diff --git a/src/marks/axis.js b/src/marks/axis.js
index 406ff70dd5..333faed9d7 100644
--- a/src/marks/axis.js
+++ b/src/marks/axis.js
@@ -1,5 +1,5 @@
 import {InternSet, extent, format, utcFormat} from "d3";
-import {formatDefault} from "../format.js";
+import {formatAuto} from "../format.js";
 import {marks} from "../mark.js";
 import {radians} from "../math.js";
 import {arrayify, constant, identity, keyword, number, range, valueof} from "../options.js";
@@ -384,9 +384,9 @@ function axisTextKy(
       ...options,
       dx: anchor === "left" ? +dx - tickSize - tickPadding + +insetLeft : +dx + +tickSize + +tickPadding - insetRight
     },
-    function (scale, data, ticks, tickFormat, channels) {
+    function (scale, data, options, channels, context) {
       if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
-      if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor);
+      if (text === undefined) channels.text = inferTextChannel(scale, data, {...options, anchor}, context);
     }
   );
 }
@@ -430,9 +430,9 @@ function axisTextKx(
       ...options,
       dy: anchor === "bottom" ? +dy + +tickSize + +tickPadding - insetBottom : +dy - tickSize - tickPadding + +insetTop
     },
-    function (scale, data, ticks, tickFormat, channels) {
+    function (scale, data, options, channels, context) {
       if (fontVariant === undefined) this.fontVariant = inferFontVariant(scale);
-      if (text === undefined) channels.text = inferTextChannel(scale, data, ticks, tickFormat, anchor);
+      if (text === undefined) channels.text = inferTextChannel(scale, data, {...options, anchor}, context);
     }
   );
 }
@@ -612,7 +612,7 @@ function axisMark(mark, k, data, properties, options, initialize) {
         channels[k] = {scale: k, value: identity};
       }
     }
-    initialize?.call(this, scale, data, ticks, tickFormat, channels);
+    initialize?.call(this, scale, data, {ticks, tickFormat}, channels, context);
     const initializedChannels = Object.fromEntries(
       Object.entries(channels).map(([name, channel]) => {
         return [name, {...channel, value: valueof(data, channel.value)}];
@@ -641,8 +641,8 @@ function inferTickCount(scale, tickSpacing) {
   return (max - min) / tickSpacing;
 }
 
-function inferTextChannel(scale, data, ticks, tickFormat, anchor) {
-  return {value: inferTickFormat(scale, data, ticks, tickFormat, anchor)};
+function inferTextChannel(scale, data, options, context) {
+  return {value: inferTickFormat(scale, data, options, context)};
 }
 
 // D3’s ordinal scales simply use toString by default, but if the ordinal scale
@@ -651,15 +651,15 @@ function inferTextChannel(scale, data, ticks, tickFormat, anchor) {
 // time ticks, we want to use the multi-line time format (e.g., Jan 26) if
 // possible, or the default ISO format (2014-01-26). TODO We need a better way
 // to infer whether the ordinal scale is UTC or local time.
-export function inferTickFormat(scale, data, ticks, tickFormat, anchor) {
+export function inferTickFormat(scale, data, {ticks, tickFormat, anchor}, {locale}) {
   return typeof tickFormat === "function" && !(scale.type === "log" && scale.tickFormat)
     ? tickFormat
     : tickFormat === undefined && data && isTemporal(data)
-    ? inferTimeFormat(scale.type, data, anchor) ?? formatDefault
+    ? inferTimeFormat(scale.type, data, anchor) ?? formatAuto(locale)
     : scale.tickFormat
     ? scale.tickFormat(typeof ticks === "number" ? ticks : null, tickFormat)
     : tickFormat === undefined
-    ? formatDefault
+    ? formatAuto(locale)
     : typeof tickFormat === "string"
     ? (isTemporal(scale.domain()) ? utcFormat : format)(tickFormat)
     : constant(tickFormat);
diff --git a/src/plot.d.ts b/src/plot.d.ts
index 05fc238dc5..98924ea97e 100644
--- a/src/plot.d.ts
+++ b/src/plot.d.ts
@@ -74,6 +74,9 @@ export interface PlotOptions extends ScaleDefaults {
 
   // other top-level options
 
+  /** The desired locale. Defaults to "en-US". */
+  locale?: string;
+
   /**
    * Custom styles to override Plot’s defaults. Styles may be specified either
    * as a string of inline styles (*e.g.*, `"color: red;"`, in the same fashion
diff --git a/src/plot.js b/src/plot.js
index a091d8d8a8..58fd43246a 100644
--- a/src/plot.js
+++ b/src/plot.js
@@ -349,7 +349,7 @@ export function plot(options = {}) {
       .attr("font-family", "initial") // fix emoji rendering in Chrome
       .text("\u26a0\ufe0f") // emoji variation selector
       .append("title")
-      .text(`${w.toLocaleString("en-US")} warning${w === 1 ? "" : "s"}. Please check the console.`);
+      .text(`${w.toLocaleString("en-US")} warning${w === 1 ? "" : "s"}. Please check the console.`); // non-localized
   }
 
   return figure;
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 907109e590..d285175ecb 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -151,6 +151,7 @@ export * from "./likert-survey.js";
 export * from "./linear-regression-cars.js";
 export * from "./linear-regression-mtcars.js";
 export * from "./linear-regression-penguins.js";
+export * from "./locale.js";
 export * from "./log-degenerate.js";
 export * from "./log-tick-format.js";
 export * from "./long-labels.js";
diff --git a/test/plots/locale.ts b/test/plots/locale.ts
new file mode 100644
index 0000000000..bbbcf43928
--- /dev/null
+++ b/test/plots/locale.ts
@@ -0,0 +1,5 @@
+import * as Plot from "@observablehq/plot";
+
+export async function localeFrAxis() {
+  return Plot.plot({locale: "fr", x: {domain: [0, 10e3]}});
+}