Skip to content

Commit abf68a2

Browse files
authored
Merge pull request #102 from CMU-313/translationUIgiven
Add “View translation” toggle in topic view
2 parents 986187f + 59be7f1 commit abf68a2

14 files changed

Lines changed: 377 additions & 187 deletions

File tree

TRANSLATION_TEST_STEPS.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Testing Translation Feature
2+
3+
## Steps to see the translation button:
4+
5+
### 1. Set environment variable for your translator service
6+
```bash
7+
export TRANSLATOR_API_BASE="https://your-translator-service-url"
8+
```
9+
10+
### 2. Build NodeBB (compile templates and JavaScript)
11+
```bash
12+
./nodebb build
13+
```
14+
15+
### 3. Restart NodeBB
16+
```bash
17+
./nodebb restart
18+
```
19+
20+
### 4. Create a NEW post with non-English content
21+
22+
**Important:** Only posts created AFTER the code changes will have the `isEnglish` field. Old posts won't show the button.
23+
24+
Test with content like:
25+
- "Hola mundo"
26+
- "Bonjour le monde"
27+
- "こんにちは世界"
28+
29+
### 5. Check the post
30+
31+
If the translator service returns `isEnglish: false`, you should see:
32+
- A blue button: "Click here to view the translated message."
33+
- Clicking it shows/hides the translated content
34+
35+
## Troubleshooting
36+
37+
### Button not showing?
38+
39+
**Check 1: Did you build?**
40+
```bash
41+
./nodebb build
42+
```
43+
44+
**Check 2: Is the post NEW?**
45+
- Delete old test posts
46+
- Create a fresh post after building
47+
48+
**Check 3: Check the database**
49+
```bash
50+
# Redis
51+
redis-cli
52+
HGETALL post:1 # Check if isEnglish and translatedContent exist
53+
```
54+
55+
**Check 4: Check browser console**
56+
- Open DevTools (F12)
57+
- Look for JavaScript errors
58+
- Check if topic.js loaded
59+
60+
**Check 5: Check if translator is being called**
61+
```bash
62+
# Look for translation logs in NodeBB output
63+
tail -f logs/output.log
64+
```
65+
66+
### Hard refresh
67+
If templates are cached:
68+
```bash
69+
# Clear browser cache or hard refresh
70+
Ctrl+Shift+R (Windows/Linux)
71+
Cmd+Shift+R (Mac)
72+
```
73+
74+
## Expected Flow
75+
76+
1. User creates post with "Hola mundo"
77+
2. `src/posts/create.js` calls `translator.translate()`
78+
3. Translator calls your microservice
79+
4. Stores `isEnglish: false` and `translatedContent: "Hello world"`
80+
5. Template checks `{{{if !posts.isEnglish }}}`
81+
6. Button appears
82+
7. Click → translation shows
83+
84+
## Debug Mode
85+
86+
Add console logs to check:
87+
88+
**In `src/posts/create.js`:**
89+
```javascript
90+
console.log('Translation result:', isEnglish, translatedContent);
91+
```
92+
93+
**In browser console:**
94+
```javascript
95+
// Check if posts have the field
96+
console.log(ajaxify.data.posts[0]);
97+
```
98+

install/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"type": "git",
99
"url": "https://github.com/NodeBB/NodeBB/"
1010
},
11-
"main": "app.js",
11+
"main": "app.js",
1212
"scripts": {
1313
"start": "node loader.js",
1414
"lint": "eslint --cache ./nodebb .",
@@ -29,12 +29,13 @@
2929
},
3030
"dependencies": {
3131
"@adactive/bootstrap-tagsinput": "0.8.2",
32-
"@fontsource/inter": "5.2.5",
33-
"@fontsource/poppins": "5.2.6",
32+
"@fontsource/inter": "5.2.8",
33+
"@fontsource/poppins": "5.2.7",
3434
"@fortawesome/fontawesome-free": "6.7.2",
3535
"@isaacs/ttlcache": "1.4.1",
3636
"@nodebb/spider-detector": "2.0.3",
3737
"@popperjs/core": "2.11.8",
38+
"@socket.io/redis-adapter": "8.3.0",
3839
"@textcomplete/contenteditable": "0.1.13",
3940
"@textcomplete/core": "0.1.13",
4041
"@textcomplete/textarea": "0.1.13",
@@ -77,6 +78,7 @@
7778
"helmet": "7.2.0",
7879
"html-to-text": "9.0.5",
7980
"imagesloaded": "5.0.0",
81+
"ioredis": "5.6.1",
8082
"ipaddr.js": "2.2.0",
8183
"jquery": "3.7.1",
8284
"jquery-deserialize": "2.0.0",
@@ -106,7 +108,7 @@
106108
"nodebb-plugin-spam-be-gone": "2.3.2",
107109
"nodebb-plugin-web-push": "0.7.4",
108110
"nodebb-rewards-essentials": "1.0.2",
109-
"nodebb-theme-harmony": "file:vendor/nodebb-theme-harmony-2.1.15",
111+
"nodebb-theme-harmony": "file:vendor/nodebb-theme-harmony-2.1.23",
110112
"nodebb-theme-lavender": "7.1.19",
111113
"nodebb-theme-peace": "2.2.43",
112114
"nodebb-theme-persona": "14.1.12",
@@ -122,7 +124,6 @@
122124
"postcss-clean": "1.2.0",
123125
"progress-webpack-plugin": "1.0.16",
124126
"prompt": "1.3.0",
125-
"ioredis": "5.6.1",
126127
"rimraf": "6.0.1",
127128
"rss": "1.2.2",
128129
"rtlcss": "4.3.0",
@@ -135,7 +136,6 @@
135136
"sitemap": "8.0.0",
136137
"socket.io": "4.8.1",
137138
"socket.io-client": "4.8.1",
138-
"@socket.io/redis-adapter": "8.3.0",
139139
"sortablejs": "1.15.6",
140140
"spdx-license-list": "6.10.0",
141141
"terser-webpack-plugin": "5.3.14",
@@ -159,9 +159,9 @@
159159
"@apidevtools/swagger-parser": "10.1.0",
160160
"@commitlint/cli": "19.8.1",
161161
"@commitlint/config-angular": "19.8.1",
162-
"coveralls": "3.1.1",
163162
"@eslint/js": "9.26.0",
164163
"@stylistic/eslint-plugin-js": "4.4.0",
164+
"coveralls": "3.1.1",
165165
"eslint-config-nodebb": "1.1.5",
166166
"eslint-plugin-import": "2.31.0",
167167
"grunt": "1.6.1",

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@
180180
"mocha": "11.2.2",
181181
"mocha-lcov-reporter": "1.3.0",
182182
"mockdate": "3.0.5",
183+
"nock": "^14.0.10",
183184
"nyc": "17.1.0",
184185
"smtp-server": "3.13.6"
185186
},

public/openapi/components/schemas/PostObject.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ PostObject:
3737
endorsed:
3838
type: boolean
3939
description: Whether this post has been endorsed by an administrator
40+
isEnglish:
41+
type: boolean
42+
description: Whether the post content is in English (determined by translator service)
43+
translatedContent:
44+
type: string
45+
description: Machine-translated English version of the post content (empty if post is already in English)
4046
timestampISO:
4147
type: string
4248
description: An ISO 8601 formatted date string (complementing `timestamp`)
@@ -180,6 +186,12 @@ PostDataObject:
180186
endorsed:
181187
type: boolean
182188
description: Whether this post has been endorsed by an administrator
189+
isEnglish:
190+
type: boolean
191+
description: Whether the post content is in English (determined by translator service)
192+
translatedContent:
193+
type: string
194+
description: Machine-translated English version of the post content (empty if post is already in English)
183195
deleted:
184196
type: number
185197
upvotes:

public/openapi/read/admin/extend/plugins.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,6 @@ get:
269269
- name
270270
- updated
271271
- latest
272-
- url
273272
- numInstalls
274273
- isCompatible
275274
- id

public/src/client/topic.js

Lines changed: 18 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
'use strict';
2+
3+
24
define('forum/topic', [
35
'forum/infinitescroll',
46
'forum/topic/threadTools',
@@ -69,14 +71,28 @@ define('forum/topic', [
6971
handleThumbs();
7072

7173
$(window).on('scroll', utils.debounce(updateTopicTitle, 250));
74+
configurePostToggle();
7275

7376
handleTopicSearch();
7477

7578
hooks.fire('action:topic.loaded', ajaxify.data);
76-
addResolved_Status();
77-
addEndorsed_Status();
7879
};
7980

81+
function configurePostToggle() {
82+
$('.topic').on('click', '.view-translated-btn', function () {
83+
// Toggle the visibility of the next .translated-content div
84+
$(this).closest('.sensitive-content-message').next('.translated-content').toggle();
85+
// Optionally, change the button text based on visibility
86+
var isVisible = $(this).closest('.sensitive-content-message').next('.translated-content').is(':visible');
87+
if (isVisible) {
88+
$(this).text('Hide the translated message.');
89+
} else {
90+
$(this).text('Click here to view the translated message.');
91+
}
92+
});
93+
}
94+
95+
8096
function handleTopicSearch() {
8197
require(['mousetrap'], (mousetrap) => {
8298
if (config.topicSearchEnabled) {
@@ -481,143 +497,5 @@ define('forum/topic', [
481497
}
482498

483499

484-
function addResolved_Status() {
485-
const firstPost = $('[component="post"][data-index="0"]');
486-
if (!firstPost.length) return;
487-
488-
const isAdmin = app.user && app.user.isAdmin;
489-
const tid = ajaxify.data.tid;
490-
491-
firstPost.prepend('<div class="resolved-loading">Loading...</div>');
492-
493-
fetch(`/api/topics/${tid}/resolved`).then(res => res.json())
494-
.then(data => {
495-
const isResolved = data.resolved;
496-
497-
const currStatus = $(`
498-
<div class="post-toggle" style="cursor: ${isAdmin ? 'pointer' : 'default'}; padding: 5px; margin: 5px; display: flex; align-items: center; gap: 8px;">
499-
<div class="checkbox" style="width: 15px; height: 15px; border: 2px solid #999; display: inline-block; text-align: center; line-height: 12px; font-size: 12px;">
500-
${isResolved ? '✓' : ''}
501-
</div>
502-
<span class="current_status" style="color: ${isResolved ? 'green' : 'red'}">
503-
${isResolved ? 'resolved' : 'unresolved'}
504-
</span>
505-
${!isAdmin ? '<span style="color: #999; font-size: 11px;">(admin only)</span>' : ''}
506-
</div>
507-
`);
508-
509-
if (isAdmin) {
510-
currStatus.on('click', function () {
511-
const checkbox = $(this).find('.checkbox');
512-
const statusText = $(this).find('.current_status');
513-
const currentResolved = checkbox.text().trim() === '✓';
514-
const newStatus = !currentResolved;
515-
516-
fetch(`${config.relative_path}/api/topics/${tid}/resolved`, {
517-
method: 'PUT',
518-
headers: {
519-
'Content-Type': 'application/json',
520-
'x-csrf-token': config.csrf_token,
521-
},
522-
credentials: 'same-origin',
523-
body: JSON.stringify({ resolved: newStatus }),
524-
})
525-
.then(res => {
526-
if (!res.ok) {
527-
return res.json().then(err => {
528-
console.log('Error response:', err);
529-
throw new Error('Failed to update');
530-
});
531-
}
532-
return res.json();
533-
})
534-
.then((data) => {
535-
console.log('Success response:', data);
536-
checkbox.text(newStatus ? '✓' : '');
537-
statusText.text(newStatus ? 'resolved' : 'unresolved');
538-
statusText.css('color', newStatus ? 'green' : 'red');
539-
alerts.success(newStatus ? 'Marked as resolved' : 'Marked as unresolved');
540-
})
541-
.catch((err) => {
542-
console.log('Catch error:', err);
543-
alerts.error('Failed to update');
544-
});
545-
});
546-
}
547-
548-
firstPost.find('.resolved-loading').replaceWith(currStatus);
549-
});
550-
}
551-
552-
function addEndorsed_Status() {
553-
const allPosts = $('[component="post"]');
554-
const isAdmin = app.user && app.user.isAdmin;
555-
556-
allPosts.each(function () {
557-
const postElem = $(this);
558-
const pid = postElem.attr('data-pid');
559-
560-
if (!pid) return;
561-
562-
postElem.prepend('<div class="endorsed-loading">Loading...</div>');
563-
564-
fetch(`/api/posts/${pid}/endorsed`, {
565-
credentials: 'same-origin',
566-
}).then(r => r.json())
567-
.then(data => {
568-
const isEndorsed = data.endorsed;
569-
570-
let endorsedStatus;
571-
572-
if (isAdmin) {
573-
// Admin sees a button with data attribute to track state
574-
endorsedStatus = $(`
575-
<button class="endorsed-button btn btn-sm" data-endorsed="${isEndorsed}" style="margin: 5px; ${isEndorsed ? 'background-color: #007bff; color: white; border: none;' : 'background-color: #f0f0f0; color: #666; border: 2px solid #007bff;'}">
576-
${isEndorsed ? 'Endorsed ✓' : 'Endorse this post?'}
577-
</button>
578-
`);
579-
580-
endorsedStatus.on('click', function () {
581-
const button = $(this);
582-
const currentState = button.attr('data-endorsed') === 'true'; // Read current state from button
583-
const newStatus = !currentState;
584-
585-
fetch(`/api/posts/${pid}/endorsed`, {
586-
method: 'PUT',
587-
headers: {
588-
'Content-Type': 'application/json',
589-
'x-csrf-token': config.csrf_token,
590-
},
591-
credentials: 'same-origin',
592-
body: JSON.stringify({ endorsed: newStatus }),
593-
}).then(r => r.json())
594-
.then(() => {
595-
button.attr('data-endorsed', newStatus); // Update state on button
596-
if (newStatus) {
597-
button.text('Endorsed ✓').css({'background-color': '#007bff', 'color': 'white', 'border': 'none'});
598-
} else {
599-
button.text('Endorse this post?').css({'background-color': '#f0f0f0', 'color': '#666', 'border': '2px solid #007bff'});
600-
}
601-
alerts.success(newStatus ? 'Post endorsed' : 'Post unendorsed');
602-
})
603-
.catch(() => alerts.error('Failed to update'));
604-
});
605-
} else {
606-
// Non-admin sees read-only status
607-
endorsedStatus = $(`
608-
<div style="padding: 5px; margin: 5px; color: ${isEndorsed ? 'blue' : '#999'}; font-size: 14px;">
609-
${isEndorsed ? 'Endorsed by admin ✓' : ''}
610-
</div>
611-
`);
612-
}
613-
614-
postElem.find('.endorsed-loading').replaceWith(endorsedStatus);
615-
});
616-
});
617-
}
618-
619-
620500
return Topic;
621501
});
622-
623-

0 commit comments

Comments
 (0)