diff --git a/CHANGELOG.md b/CHANGELOG.md
index 894e067..01cf14e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,3 +8,4 @@
- Older API responses without `years_on_record`, and spans outside 1886 to next year, are handled the same as `null`.
- A host-supplied year outside the model's span is kept (inserted in order and selected, with one `console.warn`), never dropped.
- The widget disables the year select only for `yearFallback: "none"` with nothing to choose (with `includeOther`, Other stays choosable), and only ever undoes its own disabling.
+- New option `otherLabel` (default `"Other / not listed"`): the text of the `includeOther` row on make, model and year, for pages in other languages. The option value stays `"other"`.
diff --git a/README.md b/README.md
index 6591076..03d96ed 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ VehiclesDropdown.attach({
yearFallback: "range", // a model with no register span: "range" (default) or "none"
values: "slug", // option values: "slug" (default) or "name"
includeOther: true, // append "Other / not listed" — pickers should never dead-end
+ otherLabel: "Other / not listed", // its text, to translate; the value is always "other"
search: true, // searchable combobox instead of a plain select
logos: true, // make logos in the list (search mode; free key feature)
diff --git a/demo/index.html b/demo/index.html
index 8a486c3..d656f4d 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -45,6 +45,7 @@
VehiclesDropdown demo
search: true,
logos: true,
includeOther: true,
+ otherLabel: "Other / not listed",
onChange: (value) => {
document.getElementById("state").textContent = JSON.stringify(value, null, 2)
},
diff --git a/make-model-dropdown.js b/make-model-dropdown.js
index 6eedacb..586013c 100644
--- a/make-model-dropdown.js
+++ b/make-model-dropdown.js
@@ -28,6 +28,7 @@
yearFallback: "range", // a model with no register span: "range" | "none"
values: "slug", // what option values hold: "slug" | "name"
includeOther: false, // append an "Other / not listed" escape hatch
+ otherLabel: "Other / not listed", // its text; the option value stays "other"
logos: false, // render make logos (combobox mode only)
search: false, // searchable combobox instead of a plain
attribution: true, // "Vehicle data by VehiclesDB" line (CC-BY credit)
@@ -141,8 +142,10 @@
}
}
- function appendOther(select) {
- select.add(new Option("Other / not listed", "other"))
+ // The escape hatch's TEXT is the host's to translate; its VALUE is the
+ // API contract ("other" in every payload, whatever the page's language).
+ function appendOther(select, config) {
+ select.add(new Option(config.otherLabel, "other"))
}
// A model's register span, or null. `years_on_record` reached the API
@@ -271,7 +274,7 @@
this.makes = data.makes
const preselected = this.makeSelect.value
fillSelect(this.makeSelect, this.config.placeholderMake, data.makes, this.config)
- if (this.config.includeOther) appendOther(this.makeSelect)
+ if (this.config.includeOther) appendOther(this.makeSelect, this.config)
if (this.combobox) this.combobox.setItems(data.makes)
// The preselected make (slug or name, forgiving): set the form value
@@ -321,7 +324,7 @@
const makeValue = this.makeSelect.value
if (!makeValue || makeValue === "other") {
- if (makeValue === "other" && this.config.includeOther) appendOther(this.modelSelect)
+ if (makeValue === "other" && this.config.includeOther) appendOther(this.modelSelect, this.config)
return
}
@@ -342,7 +345,7 @@
// so it can only ever describe the models actually on screen.
this.models = data.models
fillSelect(this.modelSelect, this.config.placeholderModel, data.models, this.config, "")
- if (this.config.includeOther) appendOther(this.modelSelect)
+ if (this.config.includeOther) appendOther(this.modelSelect, this.config)
// The preselected model, applied exactly once — after that the person
// is driving, and a make change must reset the model like always.
@@ -402,7 +405,7 @@
: none ? []
: yearItems(current - FALLBACK_YEARS, current)
fillSelect(select, this.config.placeholderYear, items, this.config, "")
- if (this.config.includeOther) appendOther(select)
+ if (this.config.includeOther) appendOther(select, this.config)
// The preselected year, applied exactly once and only to its intended
// make and model — after that the person is driving.
@@ -429,7 +432,7 @@
keepYear(wanted, modelValue) {
const select = this.yearSelect
if (!Array.from(select.options).some((option) => option.value && option.value === wanted)) {
- if (wanted === "other") appendOther(select)
+ if (wanted === "other") appendOther(select, this.config)
else if (/^\d+$/.test(wanted)) {
const before = Array.from(select.options).find((option) =>
option.value === "other" || (option.value && Number(option.value) < Number(wanted)))
@@ -644,7 +647,7 @@
shown = matches.slice(0, 50)
// The escape hatch rides along unfiltered: a picker should never
// dead-end, least of all for the person whose make is not listed.
- if (self.config.includeOther) shown = shown.concat({ name: "Other / not listed", value: "other" })
+ if (self.config.includeOther) shown = shown.concat({ name: self.config.otherLabel, value: "other" })
active = -1
input.removeAttribute("aria-activedescendant")
list.replaceChildren(...shown.map(row))
@@ -653,7 +656,7 @@
function selectedItem() {
if (select.value === "other" && self.config.includeOther) {
- return { name: "Other / not listed", value: "other" }
+ return { name: self.config.otherLabel, value: "other" }
}
return items.find((item) => optionValue(self.config, item) === select.value)
}
diff --git a/test/make-model-dropdown.test.js b/test/make-model-dropdown.test.js
index 129ab3f..9db4f9e 100644
--- a/test/make-model-dropdown.test.js
+++ b/test/make-model-dropdown.test.js
@@ -836,6 +836,69 @@ test("Other / not listed reaches the year step and never strands it", async () =
assert.deepEqual(changes.at(-1), { make: "other", model: null, year: null })
})
+test("otherLabel translates the escape hatch on every select; its value stays \"other\"", async () => {
+ const document = installDOM(`
+
+
+ `)
+ const base = apiBase("other-label")
+ const changes = []
+ global.fetch = async (url) => new URL(url).pathname.endsWith("/models")
+ ? jsonResponse({ models: [{ name: "A3", slug: "a3", years_on_record: { first: 2020, last: 2021 } }] })
+ : jsonResponse({ makes: [{ name: "Audi", slug: "audi" }] })
+ const lastText = (select) => select.options[select.options.length - 1].textContent
+
+ attach({
+ key: "other-label-key", apiBase: base, make: "#make", model: "#model", year: "#year",
+ includeOther: true, attribution: false
+ })
+ attach({
+ key: "other-label-key", apiBase: base, make: "#make-es", model: "#model-es", year: "#year-es",
+ includeOther: true, otherLabel: "Otra / no aparece", attribution: false,
+ onChange(value) { changes.push(value) }
+ })
+ await waitFor(() => document.querySelector("#make").options.length === 3 &&
+ document.querySelector("#make-es").options.length === 3)
+
+ for (const [suffix, label] of [["", "Other / not listed"], ["-es", "Otra / no aparece"]]) {
+ const make = document.querySelector(`#make${suffix}`)
+ const model = document.querySelector(`#model${suffix}`)
+ const year = document.querySelector(`#year${suffix}`)
+ pick(make, "audi")
+ await waitFor(() => model.options.length === 3)
+ pick(model, "a3")
+ assert.deepEqual(optionValues(year), ["", "2021", "2020", "other"])
+ for (const select of [make, model, year]) assert.equal(lastText(select), label)
+
+ // The value is the API contract, whatever the page's language.
+ pick(make, "other")
+ assert.deepEqual(optionValues(model), ["", "other"])
+ assert.equal(lastText(model), label)
+ }
+ assert.deepEqual(changes.at(-1), { make: "other", model: null, year: null })
+})
+
+test("otherLabel names the Other row in the searchable combobox too", async () => {
+ const document = installDOM(` `)
+ dom.window.HTMLElement.prototype.scrollIntoView = () => {}
+ const base = apiBase("other-label-combobox")
+ global.fetch = async () => jsonResponse({ makes: [{ name: "Audi", slug: "audi" }] })
+
+ attach({
+ key: "other-label-combobox-key", apiBase: base, make: "#make", search: true,
+ includeOther: true, otherLabel: "Inna / brak na liście", attribution: false
+ })
+ await waitFor(() => document.querySelector("#make").options.length === 3)
+ const input = document.querySelector("[role=combobox]")
+ input.dispatchEvent(new Event("focus"))
+ const rows = Array.from(document.querySelectorAll("[role=option]"))
+ assert.equal(rows.at(-1).textContent, "Inna / brak na liście")
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }))
+ input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }))
+ assert.equal(document.querySelector("#make").value, "other")
+ assert.equal(input.value, "Inna / brak na liście")
+})
+
test("a widget without a year select keeps the exact {make, model} contract", async () => {
const document = installDOM(` `)
const base = apiBase("year-absent")