Skip to content

Commit 7b2117b

Browse files
support comment translations
1 parent d3396ac commit 7b2117b

File tree

23 files changed

+614
-127
lines changed

23 files changed

+614
-127
lines changed

client-participation-alpha/TODO.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
- [x] Extract svg markup into components
1212
- [x] Break up PCA visualization into smaller components
1313
- [x] Validate a11y
14-
- [ ] Validate translations
14+
- [ ] Validate custom translations
15+
- [ ] Validate 3rd party translations
1516
- [ ] Validate mobile layout
1617
- [ ] Validate Embeds
1718
- [ ] Unit tests

client-participation-alpha/src/api/comments.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { uiLanguage } from '../lib/lang'
12
import PolisNet from '../lib/net'
23
import type { Comment, NextCommentResponse } from './types'
34

@@ -27,8 +28,10 @@ export async function fetchNextComment(
2728
conversation_id: conversationId
2829
}
2930

30-
if (lang !== undefined) {
31-
params.lang = lang
31+
// Auto-detect language only if not provided (undefined)
32+
const detectedLang = lang !== undefined ? lang : uiLanguage()
33+
if (detectedLang) {
34+
params.lang = detectedLang
3235
}
3336

3437
return await PolisNet.polisGet<NextCommentResponse>('/nextComment', params)
@@ -38,7 +41,6 @@ export async function submitComment(payload: {
3841
conversation_id: string
3942
txt: string
4043
pid: number
41-
lang?: string
4244
is_seed?: boolean
4345
vote?: number
4446
agid?: number

client-participation-alpha/src/api/participation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1+
import { uiLanguage } from '../lib/lang'
12
import PolisNet from '../lib/net'
23
import type { ParticipationInitData } from './types'
34

45
export async function fetchParticipationInit(
56
conversationId: string,
67
options: {
78
includePCA?: boolean
9+
lang?: string
810
xid?: string
911
x_name?: string
1012
x_profile_image_url?: string
1113
} = {}
1214
): Promise<ParticipationInitData> {
15+
// Auto-detect language only if not provided (undefined)
16+
const lang = options.lang !== undefined ? options.lang : uiLanguage()
1317
const params: Record<string, string | boolean | undefined> = {
1418
conversation_id: conversationId,
1519
includePCA: options.includePCA,
20+
...(lang && { lang }),
1621
xid: options.xid,
1722
x_name: options.x_name,
1823
x_profile_image_url: options.x_profile_image_url

client-participation-alpha/src/api/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,21 @@ export interface ParticipationInitData {
124124
tid: number
125125
txt: string
126126
remaining?: number
127+
lang?: string
128+
translations?: {
129+
zid: number
130+
tid: number
131+
src: number
132+
txt: string
133+
lang: string
134+
created: string
135+
modified: string
136+
}[]
127137
}
128138
auth?: {
129139
token?: string
130140
}
141+
acceptLanguage?: string
131142
[key: string]: unknown
132143
}
133144

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
import type { VoteResponse } from '../components/types'
2+
import { uiLanguage } from '../lib/lang'
23
import PolisNet from '../lib/net'
34

45
export async function submitVote(payload: {
56
agid: number
67
conversation_id: string
78
high_priority?: boolean
8-
lang: string
9+
lang?: string
910
pid: number
1011
tid: number | string
1112
vote: number
1213
}): Promise<VoteResponse> {
13-
return await PolisNet.polisPost<VoteResponse>('/votes', payload)
14+
// Auto-detect language only if not provided (undefined)
15+
// If lang is null or blank, don't auto-detect
16+
const lang = payload.lang !== undefined ? payload.lang : uiLanguage()
17+
const finalPayload = {
18+
...payload,
19+
...(lang && { lang })
20+
}
21+
return await PolisNet.polisPost<VoteResponse>('/votes', finalPayload)
1422
}

client-participation-alpha/src/components/Statement.tsx

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useState } from 'react'
2+
import { uiLanguage } from '../lib/lang'
23
import type { Translations } from '../strings/types'
34
import InfoIcon from './icons/InfoIcon'
45
import type { StatementData } from './types'
@@ -23,6 +24,59 @@ export function Statement({
2324
voteError
2425
}: StatementProps) {
2526
const [showImportanceDesc, setShowImportanceDesc] = useState<boolean>(false)
27+
const [translationsEnabled, setTranslationsEnabled] = useState<boolean>(false)
28+
29+
// Get current user language
30+
const currentLang = uiLanguage()
31+
const statementLang = statement.lang
32+
const langMismatch = statementLang && currentLang && statementLang !== currentLang
33+
34+
// Find matching translation if translations array exists
35+
const matchingTranslation = statement.translations?.find((t) => t.lang === currentLang)
36+
37+
// Determine if we have an official translation (src > 0)
38+
const hasOfficialTranslation = langMismatch && matchingTranslation && matchingTranslation.src > 0
39+
40+
// Determine if we have a non-official translation (src <= 0)
41+
const hasNonOfficialTranslation =
42+
langMismatch && matchingTranslation && matchingTranslation.src <= 0
43+
44+
// Show translation button only if:
45+
// - Translations not enabled
46+
// - Language mismatch exists
47+
// - NOT official translation
48+
// - AND (no translation exists OR translation is non-official)
49+
const shouldShowTranslationButton =
50+
!translationsEnabled &&
51+
langMismatch &&
52+
!hasOfficialTranslation &&
53+
(!matchingTranslation || hasNonOfficialTranslation)
54+
55+
// Show hide button only if:
56+
// - Translations enabled
57+
// - AND (we have a non-official translation OR no translations array)
58+
const shouldShowHideButton =
59+
translationsEnabled && (hasNonOfficialTranslation || !statement.translations)
60+
61+
// Debug logging
62+
if (
63+
typeof window !== 'undefined' &&
64+
statement.translations &&
65+
statement.translations.length > 0
66+
) {
67+
console.log('[Translation Debug]', {
68+
currentLang,
69+
statementLang,
70+
langMismatch,
71+
matchingTranslation,
72+
hasOfficialTranslation,
73+
hasNonOfficialTranslation,
74+
shouldShowTranslationButton,
75+
shouldShowHideButton,
76+
translationsEnabled,
77+
translations: statement.translations
78+
})
79+
}
2680

2781
const handleVoteClick = (voteType: number) => {
2882
if (isVoting) return
@@ -54,7 +108,72 @@ export function Statement({
54108
</span>
55109
)}
56110
</div>
57-
<p className="statement-text">{statement.txt}</p>
111+
112+
{/* Show official translation (replaces original) or original text */}
113+
{hasOfficialTranslation ? (
114+
<p className="statement-text">{matchingTranslation.txt}</p>
115+
) : (
116+
<p className="statement-text">{statement.txt}</p>
117+
)}
118+
119+
{/* Show translation buttons only if not using official translation */}
120+
{!hasOfficialTranslation && shouldShowTranslationButton && (
121+
<button
122+
className="translation-button"
123+
onClick={() => {
124+
// No-op for now
125+
setTranslationsEnabled(true)
126+
}}
127+
style={{
128+
marginTop: '0.5rem',
129+
padding: '0.5rem 1rem',
130+
fontSize: '0.875rem',
131+
backgroundColor: 'transparent',
132+
border: '1px solid var(--color-border, #ccc)',
133+
borderRadius: '4px',
134+
cursor: 'pointer',
135+
color: 'var(--color-text, #333)'
136+
}}
137+
>
138+
{s.showTranslationButton}
139+
</button>
140+
)}
141+
142+
{!hasOfficialTranslation && shouldShowHideButton && (
143+
<button
144+
className="translation-button"
145+
onClick={() => {
146+
// No-op for now
147+
setTranslationsEnabled(false)
148+
}}
149+
style={{
150+
marginTop: '0.5rem',
151+
padding: '0.5rem 1rem',
152+
fontSize: '0.875rem',
153+
backgroundColor: 'transparent',
154+
border: '1px solid var(--color-border, #ccc)',
155+
borderRadius: '4px',
156+
cursor: 'pointer',
157+
color: 'var(--color-text, #333)'
158+
}}
159+
>
160+
{s.hideTranslationButton}
161+
</button>
162+
)}
163+
164+
{/* Show non-official translation below original text when enabled */}
165+
{hasNonOfficialTranslation && translationsEnabled && (
166+
<p
167+
className="statement-text"
168+
style={{
169+
marginTop: '0.5rem',
170+
fontStyle: 'italic',
171+
color: 'var(--color-text-secondary, #666)'
172+
}}
173+
>
174+
{matchingTranslation.txt}
175+
</p>
176+
)}
58177

59178
<div className="importance-container">
60179
<label htmlFor="important">{s.importantCheckbox}</label>

client-participation-alpha/src/components/Survey.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'
22
import { fetchNextComment } from '../api/comments'
33
import { submitVote } from '../api/votes'
44
import { getConversationToken } from '../lib/auth'
5-
import { getPreferredLanguages } from '../strings/strings'
65
import type { Translations } from '../strings/types'
76
import EmailSubscribeForm from './EmailSubscribeForm'
87
import InviteCodeSubmissionForm from './InviteCodeSubmissionForm'
@@ -27,7 +26,6 @@ const submitVoteAndGetNextCommentAPI = async (
2726
agid: 1,
2827
conversation_id,
2928
high_priority,
30-
lang: getPreferredLanguages()[0],
3129
pid: decodedToken?.pid || -1,
3230
tid: vote.tid,
3331
vote: vote.vote
@@ -63,15 +61,16 @@ export default function Survey({
6361
const loadPersonalizedFirst = async () => {
6462
try {
6563
getConversationToken(conversation_id)
66-
const lang = getPreferredLanguages()[0]
67-
const resp = await fetchNextComment(conversation_id, lang)
64+
const resp = await fetchNextComment(conversation_id)
6865

6966
if (!cancelled) {
7067
if (resp && typeof resp.tid !== 'undefined') {
7168
const mapped: StatementData = {
7269
tid: resp.tid,
7370
txt: resp.txt,
74-
remaining: resp.remaining
71+
remaining: resp.remaining,
72+
lang: resp.lang,
73+
translations: resp.translations
7574
}
7675
setStatement((prev) => {
7776
if (!prev || mapped.tid !== prev.tid) {

client-participation-alpha/src/components/TreeviteInvites.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ export default function TreeviteInvites({ conversation_id, s }: TreeviteInvitesP
3737

3838
const formatDate = (iso: string) => {
3939
try {
40-
const d = new Date(iso)
41-
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
40+
const date = new Date(iso)
41+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
4242
} catch {
4343
return iso
4444
}

client-participation-alpha/src/components/topicAgenda/TopicAgenda.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const TopicAgenda = ({
5454
}, [conversation_id])
5555

5656
useEffect(() => {
57-
const f = async () => {
57+
const checkTopicPrioritize = async () => {
5858
// Check if topic prioritization is available for this conversation
5959
try {
6060
const topicPrioritizeResponse = await fetchTopicPrioritize(conversation_id)
@@ -95,7 +95,7 @@ const TopicAgenda = ({
9595
}
9696
}
9797
if (loadWidget) {
98-
f()
98+
checkTopicPrioritize()
9999
}
100100
}, [loadWidget, conversation_id])
101101

client-participation-alpha/src/components/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ export interface StatementData {
22
tid: number | string
33
txt: string
44
remaining?: number
5+
lang?: string
6+
translations?: {
7+
zid: number
8+
tid: number
9+
src: number
10+
txt: string
11+
lang: string
12+
created: string
13+
modified: string
14+
}[]
515
}
616

717
export interface VoteData {

0 commit comments

Comments
 (0)