From a1c2b7f73d7158154e1aecf4c61dd1a9ba6cac30 Mon Sep 17 00:00:00 2001 From: Quincy Morgan <2046746+quincylvania@users.noreply.github.com> Date: Mon, 14 Sep 2020 17:21:00 -0400 Subject: [PATCH] Support language-specific pluralization (re: #597, #4964) --- data/core.yaml | 85 +++++++++++---------- dist/locales/en.json | 95 ++++++++++++++---------- modules/core/localizer.js | 69 ++++++++++++++--- modules/operations/copy.js | 10 +-- modules/operations/delete.js | 2 +- modules/operations/downgrade.js | 4 +- modules/operations/extract.js | 2 +- modules/operations/paste.js | 8 +- modules/operations/split.js | 2 +- modules/ui/contributors.js | 6 +- modules/ui/panels/history.js | 2 +- modules/ui/panels/measurement.js | 2 +- modules/ui/sections/changes.js | 2 +- modules/ui/sections/entity_issues.js | 2 +- modules/ui/sections/validation_issues.js | 2 +- modules/validations/disconnected_way.js | 8 +- 16 files changed, 179 insertions(+), 122 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 903788669..3a7b49899 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -24,12 +24,6 @@ en: changes_context: "({changes}) {base} – {context}" labeled_and_more: "{labeled} and {count} more" modes: - add_feature: - title: Add a feature - description: "Search for features to add to the map." - key: Tab - result: "{count} result" - results: "{count} results" add_area: title: Area description: "Add parks, buildings, lakes or other areas to the map." @@ -83,22 +77,22 @@ en: copy: title: Copy description: - single: Set this feature for pasting. - multiple: Set these features for pasting. + one: Set this feature for pasting. + other: Set these features for pasting. annotation: - single: Copied a feature. - multiple: "Copied {n} features." + one: Copied a feature. + other: "Copied {n} features." too_large: - single: This can't be copied because not enough of it is currently visible. - multiple: These can't be copied because not enough of them are currently visible. + one: This can't be copied because not enough of it is currently visible. + other: These can't be copied because not enough of them are currently visible. paste: title: Paste description: - single: "Add a duplicate {feature} here." - multiple: "Add {n} duplicate features here." + one: "Add a duplicate {feature} here." + other: "Add {n} duplicate features here." annotation: - single: Pasted a feature. - multiple: "Pasted {n} features." + one: Pasted a feature. + other: "Pasted {n} features." nothing_copied: No features have been copied. circularize: title: Circularize @@ -200,7 +194,9 @@ en: line: Deleted a line. area: Deleted an area. relation: Deleted a relation. - multiple: "Deleted {n} features." + feature: + one: "Deleted a feature." + other: "Deleted {n} features." too_large: single: This feature can't be deleted because not enough of it is currently visible. multiple: These features can't be deleted because not enough of them are currently visible. @@ -227,12 +223,14 @@ en: address: Remove all non-address tags. annotation: building: - single: Downgraded a feature to a basic building. - multiple: "Downgraded {n} features to basic buildings." + one: Downgraded a feature to a basic building. + other: "Downgraded {n} features to basic buildings." address: - single: Downgraded a feature to an address. - multiple: "Downgraded {n} features to addresses." - multiple: "Downgraded {n} features." + one: Downgraded a feature to an address. + other: "Downgraded {n} features to addresses." + generic: + one: "Downgraded a feature." + other: "Downgraded {n} features." has_wikidata_tag: single: This feature can't be downgraded because it has a Wikidata tag. multiple: These features can't be downgraded because some have Wikidata tags. @@ -295,7 +293,9 @@ en: title: Merge description: Merge these features. key: C - annotation: "Merged {n} features." + annotation: + one: "Merged a feature." + other: "Merged {n} features." not_eligible: These features can't be merged. not_adjacent: These features can't be merged because their endpoints aren't connected. restriction: "These features can't be merged because it would damage a \"{relation}\" relation." @@ -410,7 +410,9 @@ en: annotation: line: Split a line. area: Split an area boundary. - multiple: "Split {n} lines/area boundaries." + feature: + one: "Split a feature." + other: "Split {n} features." not_eligible: Lines can't be split at their beginning or end. multiple_ways: There are too many lines here to split. connected_to_hidden: This can't be split because it is connected to a hidden feature. @@ -434,8 +436,8 @@ en: feature: multiple: Extract points from these features. annotation: - single: Extracted a point. - multiple: "Extracted {n} points." + one: Extracted a point. + other: "Extracted {n} points." too_large: single: A point can't be extracted because not enough of this feature is visible. multiple: Points can't be extracted because not enough of these features are visible. @@ -500,8 +502,9 @@ en: key: '`' tooltip: Toggle the sidebar. feature_info: - hidden_warning: "{count} hidden features" - hidden_details: "These features are currently hidden: {details}" + hidden_warning: + one: "{count} hidden feature" + other: "{count} hidden features" osm_api_status: message: error: Unable to reach the OpenStreetMap API. Your edits are safe locally. Check your network connection. @@ -516,7 +519,7 @@ en: request_review: "I would like someone to review my edits." save: Upload cancel: Cancel - changes: "Changes ({count})" + changes: Changes download_changes: Download osmChange file errors: Errors warnings: Warnings @@ -531,9 +534,14 @@ en: google_warning_link: https://www.openstreetmap.org/copyright contributors: list: "Edits by {users}" - truncated_list: "Edits by {users} and {count} others" + truncated_list: + one: "Edits by {users} and {count} other" + other: "Edits by {users} and {count} others" info_panels: key: I + selected: + one: "{n} selected" + other: "{n} selected" background: key: B title: Background @@ -551,7 +559,6 @@ en: history: key: H title: History - selected: "{n} selected" no_history: "No History (New Feature)" version: Version last_edit: Last Edit @@ -571,7 +578,6 @@ en: measurement: key: M title: Measurement - selected: "{n} selected" geometry: Geometry closed_line: closed line closed_area: closed area @@ -620,7 +626,9 @@ en: choose_relation: Choose a parent relation role: Role choose: Select feature type - results: "{n} results for {search}" + results: + one: "{n} result for {search}" + other: "{n} results for {search}" no_documentation_key: "There is no documentation available." edit_reference: "edit/translate" wiki_reference: View documentation @@ -1496,11 +1504,11 @@ en: issues: title: Issues key: I - list_title: "Issues ({count})" + list_title: "Issues" errors: - list_title: "Errors ({count})" + list_title: "Errors" warnings: - list_title: "Warnings ({count})" + list_title: "Warnings" rules: title: Rules user_resolved_issues: Issues resolved by your edits @@ -1596,10 +1604,9 @@ en: tip: "Find unroutable roads, paths, and ferry routes" routable: message: - multiple: "{count} routable features are connected only to each other." + one: "{highway} is disconnected from other roads and paths" + other: "{count} routable features are connected only to each other" reference: "All roads, paths, and ferry routes should connect to form a single routing network." - highway: - message: "{highway} is disconnected from other roads and paths" fixme_tag: message: '{feature} has a "Fix Me" request' reference: 'A "fixme" tag indicates that a mapper has requested help with a feature.' diff --git a/dist/locales/en.json b/dist/locales/en.json index 9cb966c86..3003e88ee 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -29,13 +29,6 @@ "labeled_and_more": "{labeled} and {count} more" }, "modes": { - "add_feature": { - "title": "Add a feature", - "description": "Search for features to add to the map.", - "key": "Tab", - "result": "{count} result", - "results": "{count} results" - }, "add_area": { "title": "Area", "description": "Add parks, buildings, lakes or other areas to the map.", @@ -106,27 +99,27 @@ "copy": { "title": "Copy", "description": { - "single": "Set this feature for pasting.", - "multiple": "Set these features for pasting." + "one": "Set this feature for pasting.", + "other": "Set these features for pasting." }, "annotation": { - "single": "Copied a feature.", - "multiple": "Copied {n} features." + "one": "Copied a feature.", + "other": "Copied {n} features." }, "too_large": { - "single": "This can't be copied because not enough of it is currently visible.", - "multiple": "These can't be copied because not enough of them are currently visible." + "one": "This can't be copied because not enough of it is currently visible.", + "other": "These can't be copied because not enough of them are currently visible." } }, "paste": { "title": "Paste", "description": { - "single": "Add a duplicate {feature} here.", - "multiple": "Add {n} duplicate features here." + "one": "Add a duplicate {feature} here.", + "other": "Add {n} duplicate features here." }, "annotation": { - "single": "Pasted a feature.", - "multiple": "Pasted {n} features." + "one": "Pasted a feature.", + "other": "Pasted {n} features." }, "nothing_copied": "No features have been copied." }, @@ -262,7 +255,10 @@ "line": "Deleted a line.", "area": "Deleted an area.", "relation": "Deleted a relation.", - "multiple": "Deleted {n} features." + "feature": { + "one": "Deleted a feature.", + "other": "Deleted {n} features." + } }, "too_large": { "single": "This feature can't be deleted because not enough of it is currently visible.", @@ -298,14 +294,17 @@ }, "annotation": { "building": { - "single": "Downgraded a feature to a basic building.", - "multiple": "Downgraded {n} features to basic buildings." + "one": "Downgraded a feature to a basic building.", + "other": "Downgraded {n} features to basic buildings." }, "address": { - "single": "Downgraded a feature to an address.", - "multiple": "Downgraded {n} features to addresses." + "one": "Downgraded a feature to an address.", + "other": "Downgraded {n} features to addresses." }, - "multiple": "Downgraded {n} features." + "generic": { + "one": "Downgraded a feature.", + "other": "Downgraded {n} features." + } }, "has_wikidata_tag": { "single": "This feature can't be downgraded because it has a Wikidata tag.", @@ -389,7 +388,10 @@ "title": "Merge", "description": "Merge these features.", "key": "C", - "annotation": "Merged {n} features.", + "annotation": { + "one": "Merged a feature.", + "other": "Merged {n} features." + }, "not_eligible": "These features can't be merged.", "not_adjacent": "These features can't be merged because their endpoints aren't connected.", "restriction": "These features can't be merged because it would damage a \"{relation}\" relation.", @@ -536,7 +538,10 @@ "annotation": { "line": "Split a line.", "area": "Split an area boundary.", - "multiple": "Split {n} lines/area boundaries." + "feature": { + "one": "Split a feature.", + "other": "Split {n} features." + } }, "not_eligible": "Lines can't be split at their beginning or end.", "multiple_ways": "There are too many lines here to split.", @@ -569,8 +574,8 @@ } }, "annotation": { - "single": "Extracted a point.", - "multiple": "Extracted {n} points." + "one": "Extracted a point.", + "other": "Extracted {n} points." }, "too_large": { "single": "A point can't be extracted because not enough of this feature is visible.", @@ -648,8 +653,10 @@ "tooltip": "Toggle the sidebar." }, "feature_info": { - "hidden_warning": "{count} hidden features", - "hidden_details": "These features are currently hidden: {details}" + "hidden_warning": { + "one": "{count} hidden feature", + "other": "{count} hidden features" + } }, "osm_api_status": { "message": { @@ -667,7 +674,7 @@ "request_review": "I would like someone to review my edits.", "save": "Upload", "cancel": "Cancel", - "changes": "Changes ({count})", + "changes": "Changes", "download_changes": "Download osmChange file", "errors": "Errors", "warnings": "Warnings", @@ -683,10 +690,17 @@ }, "contributors": { "list": "Edits by {users}", - "truncated_list": "Edits by {users} and {count} others" + "truncated_list": { + "one": "Edits by {users} and {count} other", + "other": "Edits by {users} and {count} others" + } }, "info_panels": { "key": "I", + "selected": { + "one": "{n} selected", + "other": "{n} selected" + }, "background": { "key": "B", "title": "Background", @@ -705,7 +719,6 @@ "history": { "key": "H", "title": "History", - "selected": "{n} selected", "no_history": "No History (New Feature)", "version": "Version", "last_edit": "Last Edit", @@ -727,7 +740,6 @@ "measurement": { "key": "M", "title": "Measurement", - "selected": "{n} selected", "geometry": "Geometry", "closed_line": "closed line", "closed_area": "closed area", @@ -782,7 +794,10 @@ "choose_relation": "Choose a parent relation", "role": "Role", "choose": "Select feature type", - "results": "{n} results for {search}", + "results": { + "one": "{n} result for {search}", + "other": "{n} results for {search}" + }, "no_documentation_key": "There is no documentation available.", "edit_reference": "edit/translate", "wiki_reference": "View documentation", @@ -1853,12 +1868,12 @@ "issues": { "title": "Issues", "key": "I", - "list_title": "Issues ({count})", + "list_title": "Issues", "errors": { - "list_title": "Errors ({count})" + "list_title": "Errors" }, "warnings": { - "list_title": "Warnings ({count})" + "list_title": "Warnings" }, "rules": { "title": "Rules" @@ -1986,12 +2001,10 @@ "tip": "Find unroutable roads, paths, and ferry routes", "routable": { "message": { - "multiple": "{count} routable features are connected only to each other." + "one": "{highway} is disconnected from other roads and paths", + "other": "{count} routable features are connected only to each other" }, "reference": "All roads, paths, and ferry routes should connect to form a single routing network." - }, - "highway": { - "message": "{highway} is disconnected from other roads and paths" } }, "fixme_tag": { diff --git a/modules/core/localizer.js b/modules/core/localizer.js index d43091f6c..4213595e4 100644 --- a/modules/core/localizer.js +++ b/modules/core/localizer.js @@ -191,25 +191,44 @@ export function coreLocalizer() { }); }; + localizer.pluralRule = function(number) { + return pluralRule(number, _localeCode); + }; + + // Returns the plural rule for the given `number` with the given `localeCode`. + // One of: `zero`, `one`, `two`, `few`, `many`, `other` + function pluralRule(number, localeCode) { + + // modern browsers have this functionality built-in + const rules = 'Intl' in window && Intl.PluralRules && new Intl.PluralRules(localeCode); + if (rules) { + return rules.select(number); + } + + // fallback to basic one/other, as in English + if (number === 1) return 'one'; + return 'other'; + } + /** * Given a string identifier, try to find that string in the current * language, and return it. This function will be called recursively * with locale `en` if a string can not be found in the requested language. * - * @param {string} s string identifier + * @param {string} stringId string identifier * @param {object?} replacements token replacements and default string * @param {string?} locale locale to use (defaults to currentLocale) * @return {string?} localized string */ - localizer.t = function(s, replacements, locale) { + localizer.t = function(stringId, replacements, locale) { locale = locale || _localeCode; // US English is the default if (locale.toLowerCase() === 'en-us') locale = 'en'; - let path = s + let path = stringId .split('.') - .map(s => s.replace(//g, '.')) + .map(stringId => stringId.replace(//g, '.')) .reverse(); let result = _localeStrings[locale]; @@ -220,24 +239,50 @@ export function coreLocalizer() { if (result !== undefined) { if (replacements) { - for (let k in replacements) { - const token = `{${k}}`; - const regex = new RegExp(token, 'g'); - result = result.replace(regex, replacements[k]); + if (typeof result === 'object' && Object.keys(result).length) { + // If plural forms are provided, dig one level deeper based on the + // first numeric token replacement provided. + const number = Object.values(replacements).find(function(value) { + return typeof value === 'number'; + }); + if (number !== undefined) { + const rule = pluralRule(number, locale); + if (result[rule]) { + result = result[rule]; + } else { + // We're pretty sure this should be a plural but no string + // could be found for the given rule. Just pick the first + // string and hope it makes sense. + result = Object.values(result)[0]; + } + } + } + if (typeof result === 'string') { + for (let k in replacements) { + const token = `{${k}}`; + const regex = new RegExp(token, 'g'); + result = result.replace(regex, replacements[k]); + } } } - return result; + if (typeof result === 'string') { + // found a localized string! + return result; + } } + // no localized string found... if (locale !== 'en') { - return localizer.t(s, replacements, 'en'); // fallback - recurse with 'en' + // Fallback to the English string since it's the only language with guaranteed 100% coverage + return localizer.t(stringId, replacements, 'en'); } if (replacements && 'default' in replacements) { - return replacements.default; // fallback - replacements.default + // Fallback to a default value if one is specified in `replacements` + return replacements.default; } - const missing = `Missing ${locale} translation: ${s}`; + const missing = `Missing ${locale} translation: ${stringId}`; if (typeof console !== 'undefined') console.error(missing); // eslint-disable-line return missing; diff --git a/modules/operations/copy.js b/modules/operations/copy.js index 9ef7adbe6..13c456b83 100644 --- a/modules/operations/copy.js +++ b/modules/operations/copy.js @@ -5,8 +5,6 @@ import { utilArrayGroupBy, utilTotalExtent } from '../util'; export function operationCopy(context, selectedIDs) { - var _multi = selectedIDs.length === 1 ? 'single' : 'multiple'; - function getFilteredIdsToCopy() { return selectedIDs.filter(function(selectedID) { var entity = context.graph().hasEntity(selectedID); @@ -115,15 +113,13 @@ export function operationCopy(context, selectedIDs) { operation.tooltip = function() { var disable = operation.disabled(); return disable ? - t('operations.copy.' + disable + '.' + _multi) : - t('operations.copy.description' + '.' + _multi); + t('operations.copy.' + disable, { n: selectedIDs.length }) : + t('operations.copy.description', { n: selectedIDs.length }); }; operation.annotation = function() { - return selectedIDs.length === 1 ? - t('operations.copy.annotation.single') : - t('operations.copy.annotation.multiple', { n: selectedIDs.length.toString() }); + return t('operations.copy.annotation', { n: selectedIDs.length }); }; diff --git a/modules/operations/delete.js b/modules/operations/delete.js index 9f711dab6..b6c3c6e24 100644 --- a/modules/operations/delete.js +++ b/modules/operations/delete.js @@ -139,7 +139,7 @@ export function operationDelete(context, selectedIDs) { operation.annotation = function() { return selectedIDs.length === 1 ? t('operations.delete.annotation.' + context.graph().geometry(selectedIDs[0])) : - t('operations.delete.annotation.multiple', { n: selectedIDs.length }); + t('operations.delete.annotation.feature', { n: selectedIDs.length }); }; diff --git a/modules/operations/downgrade.js b/modules/operations/downgrade.js index 31c374594..b9cf3800a 100644 --- a/modules/operations/downgrade.js +++ b/modules/operations/downgrade.js @@ -118,9 +118,9 @@ export function operationDowngrade(context, selectedIDs) { operation.annotation = function () { var suffix; if (downgradeType === 'building_address') { - suffix = 'multiple'; + suffix = 'generic'; } else { - suffix = downgradeType + '.' + multi; + suffix = downgradeType; } return t('operations.downgrade.annotation.' + suffix, { n: affectedFeatureCount}); }; diff --git a/modules/operations/extract.js b/modules/operations/extract.js index bcd33bc6e..b8a3309c6 100644 --- a/modules/operations/extract.js +++ b/modules/operations/extract.js @@ -79,7 +79,7 @@ export function operationExtract(context, selectedIDs) { operation.annotation = function () { - return t('operations.extract.annotation.' + _amount, { n: selectedIDs.length }); + return t('operations.extract.annotation', { n: selectedIDs.length }); }; diff --git a/modules/operations/paste.js b/modules/operations/paste.js index 297465346..30e61d222 100644 --- a/modules/operations/paste.js +++ b/modules/operations/paste.js @@ -78,16 +78,12 @@ export function operationPaste(context) { if (!ids.length) { return t('operations.paste.nothing_copied'); } - return ids.length === 1 ? - t('operations.paste.description.single', { feature: utilDisplayLabel(oldGraph.entity(ids[0]), oldGraph) }) : - t('operations.paste.description.multiple', { n: ids.length.toString() }); + return t('operations.paste.description', { feature: utilDisplayLabel(oldGraph.entity(ids[0]), oldGraph), n: ids.length }); }; operation.annotation = function() { var ids = context.copyIDs(); - return ids.length === 1 ? - t('operations.paste.annotation.single') : - t('operations.paste.annotation.multiple', { n: ids.length.toString() }); + return t('operations.paste.annotation', { n: ids.length }); }; operation.id = 'paste'; diff --git a/modules/operations/split.js b/modules/operations/split.js index 14f7cf496..e5527b84b 100644 --- a/modules/operations/split.js +++ b/modules/operations/split.js @@ -58,7 +58,7 @@ export function operationSplit(context, selectedIDs) { operation.annotation = function() { return ways.length === 1 ? t('operations.split.annotation.' + context.graph().geometry(ways[0].id)) : - t('operations.split.annotation.multiple', { n: ways.length }); + t('operations.split.annotation.feature', { n: ways.length }); }; diff --git a/modules/ui/contributors.js b/modules/ui/contributors.js index 4cfae70c0..036b2cfa6 100644 --- a/modules/ui/contributors.js +++ b/modules/ui/contributors.js @@ -44,15 +44,17 @@ export function uiContributors(context) { if (u.length > limit) { var count = d3_select(document.createElement('span')); + var othersNum = u.length - limit + 1; + count.append('a') .attr('target', '_blank') .attr('href', function() { return osm.changesetsURL(context.map().center(), context.map().zoom()); }) - .text(u.length - limit + 1); + .text(othersNum); wrap.append('span') - .html(t('contributors.truncated_list', { users: userList.html(), count: count.html() })); + .html(t('contributors.truncated_list', { n: othersNum, users: userList.html(), count: count.html() })); } else { wrap.append('span') diff --git a/modules/ui/panels/history.js b/modules/ui/panels/history.js index 56da75d59..6ba0eea1f 100644 --- a/modules/ui/panels/history.js +++ b/modules/ui/panels/history.js @@ -122,7 +122,7 @@ export function uiPanelHistory(context) { selection .append('h4') .attr('class', 'history-heading') - .text(singular || t('info_panels.history.selected', { n: selected.length })); + .text(singular || t('info_panels.selected', { n: selected.length })); if (!singular) return; diff --git a/modules/ui/panels/measurement.js b/modules/ui/panels/measurement.js index 4755c683c..5424376ca 100644 --- a/modules/ui/panels/measurement.js +++ b/modules/ui/panels/measurement.js @@ -67,7 +67,7 @@ export function uiPanelMeasurement(context) { }); heading = selected.length === 1 ? selected[0].id : - t('info_panels.measurement.selected', { n: selected.length.toLocaleString(locale) }); + t('info_panels.selected', { n: selected.length }); if (selected.length) { var extent = geoExtent(); diff --git a/modules/ui/sections/changes.js b/modules/ui/sections/changes.js index 3be5de18c..c30bfce35 100644 --- a/modules/ui/sections/changes.js +++ b/modules/ui/sections/changes.js @@ -29,7 +29,7 @@ export function uiSectionChanges(context) { .title(function() { var history = context.history(); var summary = history.difference().summary(); - return t('commit.changes', { count: summary.length }); + return t('inspector.title_count', { title: t('commit.changes'), count: summary.length }); }) .disclosureContent(renderDisclosureContent); diff --git a/modules/ui/sections/entity_issues.js b/modules/ui/sections/entity_issues.js index 60fc9c995..1f2a45228 100644 --- a/modules/ui/sections/entity_issues.js +++ b/modules/ui/sections/entity_issues.js @@ -17,7 +17,7 @@ export function uiSectionEntityIssues(context) { return _issues.length > 0; }) .title(function() { - return t('issues.list_title', { count: _issues.length }); + return t('inspector.title_count', { title: t('issues.list_title'), count: _issues.length }); }) .disclosureContent(renderDisclosureContent); diff --git a/modules/ui/sections/validation_issues.js b/modules/ui/sections/validation_issues.js index 870e8a2b5..8effe2217 100644 --- a/modules/ui/sections/validation_issues.js +++ b/modules/ui/sections/validation_issues.js @@ -19,7 +19,7 @@ export function uiSectionValidationIssues(id, severity, context) { .title(function() { if (!_issues) return ''; var issueCountText = _issues.length > 1000 ? '1000+' : String(_issues.length); - return t('issues.' + severity + 's.list_title', { count: issueCountText }); + return t('inspector.title_count', { title: t('issues.' + severity + 's.list_title'), count: issueCountText }); }) .disclosureContent(renderDisclosureContent) .shouldDisplay(function() { diff --git a/modules/validations/disconnected_way.js b/modules/validations/disconnected_way.js index 4f9afda8e..159c1f7c5 100644 --- a/modules/validations/disconnected_way.js +++ b/modules/validations/disconnected_way.js @@ -23,11 +23,9 @@ export function validationDisconnectedWay() { subtype: 'highway', severity: 'warning', message: function(context) { - if (this.entityIds.length === 1) { - var entity = context.hasEntity(this.entityIds[0]); - return entity ? t('issues.disconnected_way.highway.message', { highway: utilDisplayLabel(entity, context.graph()) }) : ''; - } - return t('issues.disconnected_way.routable.message.multiple', { count: this.entityIds.length.toString() }); + var entity = this.entityIds.length && context.hasEntity(this.entityIds[0]); + var label = entity && utilDisplayLabel(entity, context.graph()); + return t('issues.disconnected_way.routable.message', { count: this.entityIds.length, highway: label }); }, reference: showReference, entityIds: Array.from(routingIslandWays).map(function(way) { return way.id; }),