Skip to content

Commit 74fb0b3

Browse files
authored
Show topic counts for custom nav links matching /new and /unread and Add per-language translation support for custom nav links (#58)
* Show topic counts for custom nav links matching /new and /unread Custom nav links pointing to /new or /unread now display the topic count in parentheses (e.g., "New (24)"), matching the behavior of Discourse's built-in navigation items. The count is retrieved from the TopicTrackingState service and updates reactively. Links with other URLs remain unchanged. * Add per-language translation support for custom nav links * Fix prettier formatting * test: add qunit acceptance test for topic count tracking in custom nav links
1 parent b21accc commit 74fb0b3

4 files changed

Lines changed: 208 additions & 9 deletions

File tree

javascripts/discourse/initializers/init-nav-bar-additions.js

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,86 @@
11
import { withPluginApi } from "discourse/lib/plugin-api";
2+
import I18n from "I18n";
3+
4+
/**
5+
* Resolve a field value from a nav link, checking for a locale-specific
6+
* translation first. Falls back to the base field value.
7+
*/
8+
function getLocalizedField(link, fieldName) {
9+
const translations = link.translations;
10+
if (!translations || translations.length === 0) {
11+
return link[fieldName];
12+
}
13+
14+
const locale = I18n.currentLocale();
15+
16+
// Try exact match first (e.g. "pt_BR"), then prefix match (e.g. "pt")
17+
const match =
18+
translations.find((t) => t.locale === locale) ||
19+
translations.find((t) => locale.startsWith(t.locale));
20+
21+
return (match && match[fieldName]) || link[fieldName];
22+
}
23+
24+
// Built-in Discourse filter routes that have associated topic counts
25+
const COUNTED_FILTERS = {
26+
"/new": "new",
27+
"/unread": "unread",
28+
};
29+
30+
function getCountForFilter(topicTrackingState, filterType) {
31+
if (!topicTrackingState) {
32+
return 0;
33+
}
34+
35+
if (filterType === "new") {
36+
return topicTrackingState.countNew?.() ?? topicTrackingState.newCount ?? 0;
37+
}
38+
39+
if (filterType === "unread") {
40+
return (
41+
topicTrackingState.countUnread?.() ?? topicTrackingState.unreadCount ?? 0
42+
);
43+
}
44+
45+
return 0;
46+
}
247

348
export default {
449
name: "nav-links-component",
550

651
initialize() {
752
withPluginApi((api) => {
8-
for (const {
9-
display_name: displayName,
10-
title,
11-
url,
12-
} of settings.nav_links) {
13-
api.addNavigationBarItem({
53+
const topicTrackingState = api.container.lookup(
54+
"service:topic-tracking-state"
55+
);
56+
57+
for (const link of settings.nav_links) {
58+
const { display_name: displayName, url } = link;
59+
const localizedDisplayName = getLocalizedField(link, "display_name");
60+
const localizedTitle = getLocalizedField(link, "title");
61+
const filterType = COUNTED_FILTERS[url];
62+
const itemConfig = {
1463
name: `custom_${displayName.replace(/\s+/g, "-").toLowerCase()}`,
15-
displayName,
16-
title,
64+
title: localizedTitle,
1765
href: url,
1866
forceActive: (category, args, router) =>
1967
router.currentURL?.split("?")[0] === url,
20-
});
68+
};
69+
70+
if (settings.Show_counts && filterType && topicTrackingState) {
71+
// Use a function for displayName so Discourse re-evaluates it
72+
// reactively, picking up count changes from TopicTrackingState
73+
itemConfig.displayName = () => {
74+
const count = getCountForFilter(topicTrackingState, filterType);
75+
return count > 0
76+
? `${localizedDisplayName} (${count})`
77+
: localizedDisplayName;
78+
};
79+
} else {
80+
itemConfig.displayName = localizedDisplayName;
81+
}
82+
83+
api.addNavigationBarItem(itemConfig);
2184
}
2285
});
2386
},

locales/en.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,16 @@ en:
1414
url:
1515
label: URL
1616
description: The link (e.g., /latest or https://discourse.org)
17+
translations:
18+
label: Translations
19+
description: Per-language overrides for display name and title
20+
properties:
21+
locale:
22+
label: Language
23+
description: Select the language for this translation
24+
display_name:
25+
label: Display name
26+
description: Translated display name for this language
27+
title:
28+
label: Title
29+
description: Translated title (hover text) for this language

settings.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,73 @@ nav_links:
3232
min_length: 1
3333
max_length: 2048
3434
url: true
35+
translations:
36+
type: objects
37+
schema:
38+
name: translation
39+
identifier: locale
40+
properties:
41+
locale:
42+
type: enum
43+
required: true
44+
choices:
45+
- ar
46+
- be
47+
- bg
48+
- bs_BA
49+
- ca
50+
- cs
51+
- da
52+
- de
53+
- el
54+
- en
55+
- en_GB
56+
- es
57+
- et
58+
- fa_IR
59+
- fi
60+
- fr
61+
- gl
62+
- he
63+
- hr
64+
- hu
65+
- hy
66+
- id
67+
- it
68+
- ja
69+
- ko
70+
- lt
71+
- lv
72+
- nb_NO
73+
- nl
74+
- pl_PL
75+
- pt
76+
- pt_BR
77+
- ro
78+
- ru
79+
- sk
80+
- sl
81+
- sq
82+
- sr
83+
- sv
84+
- sw
85+
- te
86+
- th
87+
- tr_TR
88+
- ug
89+
- uk
90+
- ur
91+
- vi
92+
- zh_CN
93+
- zh_TW
94+
display_name:
95+
type: string
96+
validations:
97+
max_length: 100
98+
title:
99+
type: string
100+
validations:
101+
max_length: 1000
35102
Hide_dropdowns:
36103
default: false
37104
description:
@@ -40,3 +107,7 @@ Hide_default_links:
40107
default: false
41108
description:
42109
en: "Hide the default links on both mobile and desktop."
110+
Show_counts:
111+
default: false
112+
description:
113+
en: "Show topic counts for links pointing to /new or /unread (e.g. New (24))."
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
2+
import { visit } from "@ember/test-helpers";
3+
import { test } from "qunit";
4+
5+
acceptance("Custom Top Navigation Links | Topic Counts", function (needs) {
6+
needs.user();
7+
8+
let originalNavLinks;
9+
let originalShowCounts;
10+
11+
needs.hooks.beforeEach(function () {
12+
originalNavLinks = settings.nav_links;
13+
originalShowCounts = settings.Show_counts;
14+
15+
settings.nav_links = [
16+
{ display_name: "New Topics", title: "new topics", url: "/new" },
17+
{ display_name: "Unread Topics", title: "unread topics", url: "/unread" },
18+
];
19+
settings.Show_counts = true;
20+
});
21+
22+
needs.hooks.afterEach(function () {
23+
settings.nav_links = originalNavLinks;
24+
settings.Show_counts = originalShowCounts;
25+
});
26+
27+
test("shows new and unread counts in navigation items", async function (assert) {
28+
const topicTrackingState = this.owner.lookup(
29+
"service:topic-tracking-state"
30+
);
31+
topicTrackingState.countNew = () => 42;
32+
topicTrackingState.countUnread = () => 17;
33+
34+
await visit("/latest");
35+
36+
assert.ok(
37+
exists(".nav-item_custom_new-topics"),
38+
"custom navigation item for new topics is rendered"
39+
);
40+
assert
41+
.dom(".nav-item_custom_new-topics a")
42+
.hasText("New Topics (42)", "it shows the custom new topics count");
43+
44+
assert.ok(
45+
exists(".nav-item_custom_unread-topics"),
46+
"custom navigation item for unread topics is rendered"
47+
);
48+
assert
49+
.dom(".nav-item_custom_unread-topics a")
50+
.hasText("Unread Topics (17)", "it shows the custom unread topics count");
51+
});
52+
});

0 commit comments

Comments
 (0)