diff --git a/web/components/answers/add-compatibility-question-button.tsx b/web/components/answers/add-compatibility-question-button.tsx index c56b02c..f98905c 100644 --- a/web/components/answers/add-compatibility-question-button.tsx +++ b/web/components/answers/add-compatibility-question-button.tsx @@ -17,6 +17,7 @@ import {AnswerCompatibilityQuestionContent} from './answer-compatibility-questio import {uniq} from 'lodash' import {QuestionWithCountType} from 'web/hooks/use-questions' import {MAX_COMPATIBILITY_QUESTION_LENGTH} from 'common/profiles/constants' +import {useT} from 'web/lib/locale' export function AddCompatibilityQuestionButton(props: { refreshCompatibilityAll: () => void @@ -25,6 +26,7 @@ export function AddCompatibilityQuestionButton(props: { const [open, setOpen] = useState(false) const user = useUser() if (!user) return null + const t = useT() return ( <> void }) { const { afterAddQuestion, setOpen } = props + const t = useT() const [question, setQuestion] = useState('') const [options, setOptions] = useState(['', '']) const [loading, setLoading] = useState(false) @@ -144,7 +147,7 @@ function CreateCompatibilityModalContent(props: { } track('create compatibility question') } catch (e) { - toast.error('Error creating compatibility question. Try again?') + toast.error(t('answers.add.error_create', 'Error creating compatibility question. Try again?')) } }) @@ -152,7 +155,8 @@ function CreateCompatibilityModalContent(props: { {options.map((o, index) => ( @@ -171,7 +176,7 @@ function CreateCompatibilityModalContent(props: { value={options[index]} onChange={(e) => onOptionChange(index, e.target.value)} className="w-full" - placeholder={`Option ${index + 1}`} + placeholder={t('answers.add.option_placeholder', 'Option {n}', { n: String(index + 1) })} rows={1} maxLength={MAX_ANSWER_LENGTH} /> @@ -188,7 +193,7 @@ function CreateCompatibilityModalContent(props: { @@ -201,7 +206,7 @@ function CreateCompatibilityModalContent(props: { setOpen(false) }} > - Cancel + {t('settings.action.cancel', 'Cancel')} diff --git a/web/components/answers/answer-compatibility-question-button.tsx b/web/components/answers/answer-compatibility-question-button.tsx index bbfdd85..4483ed5 100644 --- a/web/components/answers/answer-compatibility-question-button.tsx +++ b/web/components/answers/answer-compatibility-question-button.tsx @@ -7,6 +7,7 @@ import {Modal, MODAL_CLASS} from 'web/components/layout/modal' import {AnswerCompatibilityQuestionContent} from './answer-compatibility-question-content' import router from "next/router"; import Link from "next/link"; +import {useT} from 'web/lib/locale' export function AnswerCompatibilityQuestionButton(props: { user: User | null | undefined @@ -27,11 +28,12 @@ export function AnswerCompatibilityQuestionButton(props: { if (otherQuestions.length === 0) return null const isCore = otherQuestions.some((q) => q.importance_score === 0) const questionsToAnswer = isCore ? otherQuestions.filter((q) => q.importance_score === 0) : otherQuestions + const t = useT() return ( <> {size === 'md' ? ( )} View List of Questions + >{t('answers.answer.view_list', 'View List of Questions')} ) } @@ -75,13 +78,14 @@ export function AnswerSkippedCompatibilityQuestionsButton(props: { const {user, skippedQuestions, refreshCompatibilityAll} = props const [open, setOpen] = useState(false) if (!user) return null + const t = useT() return ( <> , @@ -72,6 +73,7 @@ export const submitCompatibilityAnswer = async ( }) } catch (error) { console.error('Failed to set compatibility answer:', error) + // Note: toast not localized here due to lack of hook; callers may handle UI feedback toast.error('Error submitting. Try again?') } } @@ -86,6 +88,7 @@ export const deleteCompatibilityAnswer = async ( await track('delete compatibility question', {id}) } catch (error) { console.error('Failed to delete prompt answer:', error) + // Note: toast not localized here due to lack of hook; callers may handle UI feedback toast.error('Error deleting. Try again?') } } @@ -122,6 +125,7 @@ export function AnswerCompatibilityQuestionContent(props: { index, total, } = props + const t = useT() const [answer, setAnswer] = useState( (props.answer as CompatibilityAnswerSubmitType) ?? getEmptyAnswer(user.id, compatibilityQuestion.id) @@ -175,7 +179,7 @@ export function AnswerCompatibilityQuestionContent(props: { {shortenedPopularity && ( {shortenedPopularity} @@ -190,7 +194,7 @@ export function AnswerCompatibilityQuestionContent(props: { )} > - Your answer + {t('answers.content.your_answer', 'Your answer')} @@ -200,7 +204,7 @@ export function AnswerCompatibilityQuestionContent(props: { /> - Answers you'll accept + {t('answers.content.answers_you_accept', "Answers you'll accept")} @@ -210,7 +214,7 @@ export function AnswerCompatibilityQuestionContent(props: { /> - Importance + {t('answers.content.importance', 'Importance')} - Your thoughts (optional, but recommended) + {t('answers.content.your_thoughts', 'Your thoughts (optional, but recommended)')} - Skip + {t('answers.content.skip', 'Skip')} )} diff --git a/web/components/answers/compatibility-question-preferred-list.tsx b/web/components/answers/compatibility-question-preferred-list.tsx index 25dd996..4506234 100644 --- a/web/components/answers/compatibility-question-preferred-list.tsx +++ b/web/components/answers/compatibility-question-preferred-list.tsx @@ -6,6 +6,7 @@ import clsx from 'clsx' import { User } from 'common/user' import { shortenName } from 'web/components/widgets/user-link' import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/outline' +import {useT} from 'web/lib/locale' export function PreferredList(props: { question: QuestionWithCountType @@ -16,6 +17,7 @@ export function PreferredList(props: { }) { const { question, answer, comparedAnswer, comparedUser, isComparedUser } = props + const t = useT() const { multiple_choice_options } = question if (!multiple_choice_options) return null const sortedEntries = Object.entries(multiple_choice_options).sort( @@ -54,8 +56,9 @@ export function PreferredList(props: { ) : ( )} - {isComparedUser ? 'Your' : shortenName(comparedUser.name) + "'s"}{' '} - answer + {isComparedUser + ? t('answers.preferred.your_answer', 'Your answer') + : t('answers.preferred.user_answer', "{name}'s answer", { name: shortenName(comparedUser.name) })} )} diff --git a/web/components/answers/compatibility-questions-display.tsx b/web/components/answers/compatibility-questions-display.tsx index 72aabf5..42421dc 100644 --- a/web/components/answers/compatibility-questions-display.tsx +++ b/web/components/answers/compatibility-questions-display.tsx @@ -45,6 +45,7 @@ import {buildArray} from 'common/util/array' import toast from "react-hot-toast"; import {useCompatibleProfiles} from "web/hooks/use-profiles"; import {CompatibleBadge} from "web/components/widgets/compatible-badge"; +import {useT} from 'web/lib/locale' const NUM_QUESTIONS_TO_SHOW = 8 @@ -84,6 +85,7 @@ export function CompatibilityQuestionsDisplay(props: { fromProfilePage?: Profile }) { const {isCurrentUser, user, fromSignup, fromProfilePage, profile} = props + const t = useT() const currentUser = useUser() const compatibleProfiles = useCompatibleProfiles(currentUser?.id) @@ -175,9 +177,11 @@ export function CompatibilityQuestionsDisplay(props: { - {`${ - isCurrentUser ? 'Your' : shortenName(user.name) + `'s` - } Compatibility Prompts`} + + {isCurrentUser + ? t('answers.display.your_prompts', 'Your Compatibility Prompts') + : t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { name: shortenName(user.name) })} + {compatibilityScore && } @@ -194,10 +198,11 @@ export function CompatibilityQuestionsDisplay(props: { {answeredQuestions.length <= 0 ? ( - {isCurrentUser ? "You haven't" : `${user.name} hasn't`} answered any - compatibility questions yet!{' '} + {isCurrentUser + ? t('answers.display.none_answered_you', "You haven't answered any compatibility questions yet!") + : t('answers.display.none_answered_user', "{name} hasn't answered any compatibility questions yet!", { name: user.name })}{' '} {isCurrentUser && ( - <>Add some to better see who you'd be most compatible with. + <>{t('answers.display.add_some', "Add some to better see who you'd be most compatible with.")} )} ) : ( @@ -206,11 +211,11 @@ export function CompatibilityQuestionsDisplay(props: { {otherQuestions.length < 1 ? ( - You've already answered all the compatibility questions— + {t('answers.display.already_answered_all', "You've already answered all the compatibility questions—")} ) : ( - Answer more questions to increase your compatibility scores—or{' '} + {t('answers.display.answer_more', 'Answer more questions to increase your compatibility scores—or ')} )} None +
{t('answers.display.none', 'None')}
)} )} @@ -279,13 +284,14 @@ function CompatibilitySortWidget(props: { const {sort, setSort, user, fromProfilePage, className} = props const currentUser = useUser() + const t = useT() const sortToDisplay = { 'your-important': fromProfilePage - ? `Important to ${fromProfilePage.user.name}` - : 'Important to you', - 'their-important': `Important to ${user.name}`, - disagree: 'Incompatible', - 'your-unanswered': 'Unanswered by you', + ? t('answers.sort.important_to_user', 'Important to {name}', { name: fromProfilePage.user.name }) + : t('answers.sort.important_to_you', 'Important to you'), + 'their-important': t('answers.sort.important_to_them', 'Important to {name}', { name: user.name }), + disagree: t('answers.sort.incompatible', 'Incompatible'), + 'your-unanswered': t('answers.sort.unanswered_by_you', 'Unanswered by you'), } const shownSorts = buildArray( @@ -339,6 +345,7 @@ export function CompatibilityAnswerBlock(props: { const [editOpen, setEditOpen] = useState(false) const currentUser = useUser() const currentProfile = useProfile() + const t = useT() const [newAnswer, setNewAnswer] = useState(props.answer) @@ -408,12 +415,12 @@ export function CompatibilityAnswerBlock(props: { , onClick: () => setEditOpen(true), }, { - name: 'Delete', + name: t('answers.menu.delete', 'Delete'), icon: , onClick: () => { deleteCompatibilityAnswer(answer.id, user.id) @@ -436,7 +443,7 @@ export function CompatibilityAnswerBlock(props: { , onClick: () => { submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id)) @@ -469,9 +476,9 @@ export function CompatibilityAnswerBlock(props: { {distinctPreferredAnswersText.length > 0 && (
- {preferredDoesNotIncludeAnswerText - ? 'Acceptable' - : 'Also acceptable'} + {preferredDoesNotIncludeAnswerText + ? t('answers.display.acceptable', 'Acceptable') + : t('answers.display.also_acceptable', 'Also acceptable')}
{distinctPreferredAnswersText.map((text) => ( @@ -635,7 +642,7 @@ function CompatibilityDisplay(props: { : 'bg-red-500/20 hover:bg-red-500/30' )} > - {answerCompatibility ? 'Compatible' : 'Incompatible'} + {answerCompatibility ? t('answers.compatible', 'Compatible') : t('answers.incompatible', 'Incompatible')} )} @@ -644,10 +651,10 @@ function CompatibilityDisplay(props: { {question.question}
- {`${shortenName(user1.name)}'s preferred answers`} + {t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user1.name) })}
- {shortenName(user1.name)} marked this as{' '} + {t('answers.modal.user_marked', '{name} marked this as ', { name: shortenName(user1.name) })} @@ -666,13 +673,14 @@ function CompatibilityDisplay(props: { />
- {`${ - isCurrentUser ? 'Your' : shortenName(user2.name) + `'s` - } preferred answers`} + {isCurrentUser + ? t('answers.modal.your_preferred', 'Your preferred answers') + : t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user2.name) })}
- {isCurrentUser ? 'You' : shortenName(user2.name)} marked this - as{' '} + {isCurrentUser + ? t('answers.modal.you_marked', 'You marked this as ') + : t('answers.modal.user_marked', '{name} marked this as ', { name: shortenName(user2.name) })} diff --git a/web/components/answers/free-response-add-question.tsx b/web/components/answers/free-response-add-question.tsx index 17b03e2..e5f5d2d 100644 --- a/web/components/answers/free-response-add-question.tsx +++ b/web/components/answers/free-response-add-question.tsx @@ -10,6 +10,7 @@ import {IndividualQuestionRow} from '../questions-form' import {TbMessage} from 'react-icons/tb' import {OtherProfileAnswers} from './other-profile-answers' import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state' +import {useT} from 'web/lib/locale' export function AddQuestionButton(props: { isFirstQuestion?: boolean @@ -22,12 +23,13 @@ export function AddQuestionButton(props: { false, `add-question-${user.id}` ) + const t = useT() return ( <> @@ -91,7 +94,7 @@ function AddQuestionModal(props: { ) : selectedQuestion == null ? ( <>
- Choose a question to answer + {t('answers.free.choose_question', 'Choose a question to answer')}
{addableQuestions.map((question) => { diff --git a/web/components/answers/free-response-display.tsx b/web/components/answers/free-response-display.tsx index eb65df3..dc7920b 100644 --- a/web/components/answers/free-response-display.tsx +++ b/web/components/answers/free-response-display.tsx @@ -27,6 +27,7 @@ import { partition } from 'lodash' import { shortenName } from 'web/components/widgets/user-link' import { AddQuestionButton } from './free-response-add-question' import { Profile } from 'common/profiles/profile' +import {useT} from 'web/lib/locale' export function FreeResponseDisplay(props: { isCurrentUser: boolean @@ -34,6 +35,7 @@ export function FreeResponseDisplay(props: { fromProfilePage: Profile | undefined }) { const { isCurrentUser, user, fromProfilePage } = props + const t = useT() const { refreshAnswers, answers: allAnswers } = useUserAnswers(user?.id) @@ -59,9 +61,11 @@ export function FreeResponseDisplay(props: { return ( - {`${ - isCurrentUser ? 'Your' : shortenName(user.name) + `'s` - } Free Response`} + + {isCurrentUser + ? t('answers.free.your_title', 'Your Free Response') + : t('answers.free.user_title', "{name}'s Free Response", { name: shortenName(user.name) })} + @@ -101,6 +105,7 @@ function AnswerBlock(props: { const { answer, questions, isCurrentUser, user, refreshAnswers } = props const question = questions.find((q) => q.id === answer.question_id) const [edit, setEdit] = useState(false) + const t = useT() const [otherAnswerModal, setOtherAnswerModal] = useState(false) @@ -119,18 +124,18 @@ function AnswerBlock(props: { , onClick: () => setEdit(true), }, { - name: 'Delete', + name: t('answers.menu.delete', 'Delete'), icon: , onClick: () => deleteAnswer(answer, user.id).then(() => refreshAnswers()), }, { - name: `See ${question.answer_count} other answers`, + name: t('answers.free.see_others', 'See {count} other answers', { count: String(question.answer_count) }), icon: , onClick: () => setOtherAnswerModal(true), }, diff --git a/web/components/answers/opinion-scale-display.tsx b/web/components/answers/opinion-scale-display.tsx index ab091b5..2558161 100644 --- a/web/components/answers/opinion-scale-display.tsx +++ b/web/components/answers/opinion-scale-display.tsx @@ -10,6 +10,7 @@ import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' import { Subtitle } from '../widgets/profile-subtitle' import { BiTachometer } from 'react-icons/bi' +import {useT} from 'web/lib/locale' export function OpinionScale(props: { multiChoiceAnswers: rowFor<'compatibility_answers_free'>[] @@ -17,6 +18,7 @@ export function OpinionScale(props: { isCurrentUser: boolean }) { const { multiChoiceAnswers, questions, isCurrentUser } = props + const t = useT() const answeredMultiChoice = multiChoiceAnswers.filter( (a) => a.multiple_choice != null && a.multiple_choice != -1 @@ -28,7 +30,7 @@ export function OpinionScale(props: { ) @@ -39,7 +41,7 @@ export function OpinionScale(props: { return ( - Opinion Scale + {t('answers.opinion.title', 'Opinion Scale')} {isCurrentUser && ( )} diff --git a/web/lib/locale.ts b/web/lib/locale.ts index 2305d86..961212a 100644 --- a/web/lib/locale.ts +++ b/web/lib/locale.ts @@ -9,7 +9,8 @@ export type I18nContextType = { export const I18nContext = createContext({ locale: defaultLocale, - setLocale: () => {} + setLocale: () => { + } }) export function useLocale() { @@ -47,5 +48,17 @@ export function useT() { .catch(() => setMessages({})) }, [locale]) - return (key: string, fallback: string) => locale === defaultLocale ? fallback : messages[key] ?? fallback + return (key: string, fallback: string, formatter?: any) => { + const result = locale === defaultLocale ? fallback : messages[key] ?? fallback + if (!formatter) return result + if (typeof formatter === 'function') return formatter(result) + if (typeof formatter === 'object') { + let text = String(result) + for (const [k, v] of Object.entries(formatter)) { + text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)) + } + return text + } + return result + } } diff --git a/web/messages/fr.json b/web/messages/fr.json index 504db5b..7fddf42 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -481,5 +481,27 @@ "block_user.toast.success": "Vous ne verrez plus le contenu de cet utilisateur", "block_user.toast.error": "Erreur lors du blocage de l'utilisateur", "block_user.unblock": "Débloquer", - "block_user.block": "Bloquer" + "block_user.block": "Bloquer", + "answers.add.submit_own": "Proposez la vôtre !", + "answers.add.error_create": "Erreur lors de la création de la question de compatibilité. Réessayez ?", + "answers.answer.cta": "Répondre{core} aux questions", + "answers.answer.answer_yourself": "Répondez vous-même", + "answers.answer.view_list": "Voir la liste des questions", + "answers.answer.answer_skipped": "Répondre à {n} questions ignorées", + "answers.preferred.your_answer": "Votre réponse", + "answers.preferred.user_answer": "La réponse de {name}", + "answers.menu.edit": "Modifier", + "answers.menu.delete": "Supprimer", + "answers.menu.skip": "Ignorer", + "answers.compatible": "Compatible", + "answers.incompatible": "Incompatible", + "answers.modal.preferred_of_user": "Les réponses préférées de {name}", + "answers.modal.user_marked": "{name} a marqué ceci comme ", + "answers.modal.your_preferred": "Vos réponses préférées", + "answers.modal.you_marked": "Vous avez marqué ceci comme ", + "answers.opinion.fill": "Remplir l'échelle d'opinion", + "answers.opinion.edit": "Modifier", + "answers.free.add_free_response": "Ajouter une réponse libre", + "answers.free.choose_question": "Choisissez une question à laquelle répondre", + "answers.free.see_others": "Voir {count} autres réponses" } \ No newline at end of file