From 7bc86224e942df1304d0532b57256e87ad5d55f3 Mon Sep 17 00:00:00 2001 From: Rudy Mizrahi Celekli <47457359+rudycelekli@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:28:09 -0400 Subject: [PATCH] fix(route): phrase ramp and lane modifiers as directions (#1343) Correct ramp and lane instructions consistently in server output and localized browser text. Contribution by @rudycelekli. --- changelog.d/1343-route-ramp-lane-wording.md | 4 ++ docs/CURRENT-STATE.md | 4 ++ src/data/directions.test.mjs | 43 +++++++++++++++++++++ src/data/routeSteps.js | 33 ++++++++++++---- src/data/routeSteps.test.mjs | 20 ++++++++++ src/i18n/locales/es.json | 4 ++ src/i18n/locales/ru.json | 4 ++ src/i18n/locales/zh-CN.json | 4 ++ src/layers/directions/index.js | 24 +++++++++--- 9 files changed, 127 insertions(+), 13 deletions(-) create mode 100644 changelog.d/1343-route-ramp-lane-wording.md diff --git a/changelog.d/1343-route-ramp-lane-wording.md b/changelog.d/1343-route-ramp-lane-wording.md new file mode 100644 index 000000000..4f6817e9c --- /dev/null +++ b/changelog.d/1343-route-ramp-lane-wording.md @@ -0,0 +1,4 @@ +- Phrase slight and sharp ramps as directions, so a ramp reads "slightly left" + instead of "on the slightly left", and a lane names its side in both + the proxy response and localized browser instructions + (Rudy Celekli, @rudycelekli, #1343). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index b0865b7b0..cc4e7b861 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -4150,6 +4150,10 @@ and as the last line of the list, because the arrival step is among the ones dropped. A route of exactly 200 is complete, and a roundabout exit after the 200th keeps folding into it. +Directions ramp and exit text uses “on the left/right” only for a bare side; +slight and sharp modifiers read as “slightly/sharply left/right”. Lane instructions +name the left or right lane. The localized page and proxy English stay in sync. + Route duration labels select their unit after rounding seconds, switching to minutes at 59.5 seconds; a 59.6-second leg displays as `1 min`, rather than `60 s`. diff --git a/src/data/directions.test.mjs b/src/data/directions.test.mjs index b0c7503ac..bf6e2878e 100644 --- a/src/data/directions.test.mjs +++ b/src/data/directions.test.mjs @@ -876,3 +876,46 @@ test('route numbers follow the page language and never break from their unit', ( const list = directionsStepList({ route }); assert.equal(list.items[1].lead, '240\u00a0м'); }); + +test('ramp and lane wording stays localized after choosing the maneuver form', (t) => { + t.after(() => setLocale('en')); + const cases = [ + [ + 'ru', + 'Въезжайте по съезду плавно налево на I-95', + 'Сверните на съезд резко направо', + 'Займите левую полосу', + ], + [ + 'es', + 'Toma la rampa ligeramente a la izquierda hacia I-95', + 'Toma la salida bruscamente a la derecha', + 'Toma el carril izquierdo', + ], + [ + 'zh-CN', + '向左前方驶入匝道,进入 I-95', + '从出口向右后方驶出', + '使用左转车道', + ], + ]; + for (const [locale, ramp, exit, lane] of cases) { + setLocale(locale); + assert.equal( + directionsInstruction({ + type: 'on ramp', + modifier: 'slight left', + name: 'I-95', + }), + ramp, + ); + assert.equal( + directionsInstruction({ type: 'off ramp', modifier: 'sharp right' }), + exit, + ); + assert.equal( + directionsInstruction({ type: 'use lane', modifier: 'sharp left' }), + lane, + ); + } +}); diff --git a/src/data/routeSteps.js b/src/data/routeSteps.js index e3144bdf7..736d812ee 100644 --- a/src/data/routeSteps.js +++ b/src/data/routeSteps.js @@ -54,6 +54,21 @@ function direction(modifier) { return DIRECTION_WORDS[key] || key; } +/** + * "on the left" only fits a bare side. "slight left" is the adverb + * "slightly left", and "on the slightly left" is not an instruction. + * @param {string} prefix "Take the ramp" or "Take the exit". + * @param {string} modifier OSRM maneuver modifier. + * @param {string} onto " onto …", or "". + */ +function directedWay(prefix, modifier, onto) { + const dir = direction(modifier); + if (dir === 'left' || dir === 'right') + return `${prefix} on the ${dir}${onto}`; + if (dir && dir !== 'straight') return `${prefix} ${dir}${onto}`; + return `${prefix}${onto}`; +} + function ordinal(n) { if (!Number.isInteger(n) || n < 1) return null; const suffix = @@ -103,13 +118,9 @@ export function instructionFor(step) { case 'merge': return dir ? `Merge ${dir}${onto}` : `Merge${onto}`; case 'on ramp': - return dir - ? `Take the ramp on the ${dir}${onto}` - : `Take the ramp${onto}`; + return directedWay('Take the ramp', modifier, onto); case 'off ramp': - return dir - ? `Take the exit on the ${dir}${onto}` - : `Take the exit${onto}`; + return directedWay('Take the exit', modifier, onto); case 'roundabout': case 'rotary': { const exit = ordinal(step?.exit); @@ -124,8 +135,14 @@ export function instructionFor(step) { case 'exit roundabout': case 'exit rotary': return `Exit the roundabout${onto}`; - case 'use lane': - return dir ? `Use the ${dir} lane${onto}` : `Continue${onto}`; + case 'use lane': { + const lane = modifier.includes('left') + ? 'left' + : modifier.includes('right') + ? 'right' + : ''; + return lane ? `Use the ${lane} lane${onto}` : `Continue${onto}`; + } case 'notification': default: return `Continue${onto}`; diff --git a/src/data/routeSteps.test.mjs b/src/data/routeSteps.test.mjs index 6bf74dd08..c0eb445dc 100644 --- a/src/data/routeSteps.test.mjs +++ b/src/data/routeSteps.test.mjs @@ -77,6 +77,26 @@ test('instructions read like a navigator, one sentence per OSRM maneuver', () => instructionFor({ type: 'on ramp', modifier: 'left' }), 'Take the ramp on the left', ); + assert.equal( + instructionFor({ type: 'on ramp', modifier: 'slight left', name: 'I-95' }), + 'Take the ramp slightly left onto I-95', + ); + assert.equal( + instructionFor({ type: 'off ramp', modifier: 'sharp right', ref: 'I-280' }), + 'Take the exit sharply right onto I-280', + ); + assert.equal( + instructionFor({ type: 'on ramp', modifier: 'straight', name: 'US 101' }), + 'Take the ramp onto US 101', + ); + assert.equal( + instructionFor({ type: 'use lane', modifier: 'slight left' }), + 'Use the left lane', + ); + assert.equal( + instructionFor({ type: 'use lane', modifier: 'right', name: 'Main St' }), + 'Use the right lane onto Main St', + ); assert.equal( instructionFor({ type: 'merge', diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 4fe130f23..098c82f55 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1867,11 +1867,15 @@ "maneuver|Merge": "Incorpórate", "Merge onto {road}": "Incorpórate a {road}", "Take the ramp on the {direction}": "Toma la rampa {direction}", + "Take the ramp {direction}": "Toma la rampa {direction}", "Take the ramp on the {direction} onto {road}": "Toma la rampa {direction} hacia {road}", + "Take the ramp {direction} onto {road}": "Toma la rampa {direction} hacia {road}", "Take the ramp": "Toma la rampa", "Take the ramp onto {road}": "Toma la rampa hacia {road}", "Take the exit on the {direction}": "Toma la salida {direction}", + "Take the exit {direction}": "Toma la salida {direction}", "Take the exit on the {direction} onto {road}": "Toma la salida {direction} hacia {road}", + "Take the exit {direction} onto {road}": "Toma la salida {direction} hacia {road}", "Take the exit": "Toma la salida", "Take the exit onto {road}": "Toma la salida hacia {road}", "At the roundabout, take the {exit} exit": "En la rotonda, toma la {exit} salida", diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json index 978640951..8d12ba43e 100644 --- a/src/i18n/locales/ru.json +++ b/src/i18n/locales/ru.json @@ -1899,11 +1899,15 @@ "maneuver|Merge": "Перестройтесь", "Merge onto {road}": "Въезжайте на {road}", "Take the ramp on the {direction}": "Въезжайте на съезд {direction}", + "Take the ramp {direction}": "Въезжайте на съезд {direction}", "Take the ramp on the {direction} onto {road}": "Въезжайте по съезду {direction} на {road}", + "Take the ramp {direction} onto {road}": "Въезжайте по съезду {direction} на {road}", "Take the ramp": "Въезжайте на съезд", "Take the ramp onto {road}": "Въезжайте по съезду на {road}", "Take the exit on the {direction}": "Сверните на съезд {direction}", + "Take the exit {direction}": "Сверните на съезд {direction}", "Take the exit on the {direction} onto {road}": "Сверните на съезд {direction} на {road}", + "Take the exit {direction} onto {road}": "Сверните на съезд {direction} на {road}", "Take the exit": "Сверните на съезд", "Take the exit onto {road}": "Сверните на съезд на {road}", "At the roundabout, take the {exit} exit": "На кольце сверните на {exit} съезд", diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 01c495a27..e727b8516 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -626,11 +626,15 @@ "Merge onto {road}": "并入 {road}", "maneuver|Merge": "并道", "Take the ramp on the {direction} onto {road}": "{direction}驶入匝道,进入 {road}", + "Take the ramp {direction} onto {road}": "{direction}驶入匝道,进入 {road}", "Take the ramp on the {direction}": "{direction}驶入匝道", + "Take the ramp {direction}": "{direction}驶入匝道", "Take the ramp onto {road}": "驶入匝道,进入 {road}", "Take the ramp": "驶入匝道", "Take the exit on the {direction} onto {road}": "从出口{direction}驶出,进入 {road}", + "Take the exit {direction} onto {road}": "从出口{direction}驶出,进入 {road}", "Take the exit on the {direction}": "从出口{direction}驶出", + "Take the exit {direction}": "从出口{direction}驶出", "Take the exit onto {road}": "从出口驶出,进入 {road}", "Take the exit": "从出口驶出", "At the roundabout, take the {exit} exit onto {road}": "在环岛从{exit}出口驶出,进入 {road}", diff --git a/src/layers/directions/index.js b/src/layers/directions/index.js index e90e1be18..b8cceff02 100644 --- a/src/layers/directions/index.js +++ b/src/layers/directions/index.js @@ -308,16 +308,24 @@ export function directionsInstruction(step) { ? t('Merge onto {road}', values) : t('Merge', { context: 'maneuver' }); case 'on ramp': - if (direction) + if (modifier === 'left' || modifier === 'right') return road ? t('Take the ramp on the {direction} onto {road}', values) : t('Take the ramp on the {direction}', values); + if (direction && modifier !== 'straight') + return road + ? t('Take the ramp {direction} onto {road}', values) + : t('Take the ramp {direction}', values); return road ? t('Take the ramp onto {road}', values) : t('Take the ramp'); case 'off ramp': - if (direction) + if (modifier === 'left' || modifier === 'right') return road ? t('Take the exit on the {direction} onto {road}', values) : t('Take the exit on the {direction}', values); + if (direction && modifier !== 'straight') + return road + ? t('Take the exit {direction} onto {road}', values) + : t('Take the exit {direction}', values); return road ? t('Take the exit onto {road}', values) : t('Take the exit'); case 'roundabout': case 'rotary': @@ -346,9 +354,14 @@ export function directionsInstruction(step) { return road ? t('Exit the roundabout onto {road}', values) : t('Exit the roundabout'); - case 'use lane': - if (direction) { - const lane = { road, direction: laneWord(modifier) }; + case 'use lane': { + const side = modifier.includes('left') + ? 'left' + : modifier.includes('right') + ? 'right' + : ''; + if (side) { + const lane = { road, direction: laneWord(side) }; return road ? t('Use the {direction} lane onto {road}', lane) : t('Use the {direction} lane', lane); @@ -356,6 +369,7 @@ export function directionsInstruction(step) { return road ? t('Continue onto {road}', values) : t('Continue', { context: 'maneuver' }); + } default: return road ? t('Continue onto {road}', values)