Translate compat questions

This commit is contained in:
MartinBraquet
2026-01-02 15:38:34 +02:00
parent bc672db79a
commit 04b8e21769
10 changed files with 132 additions and 63 deletions

View File

@@ -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 (
<>
<button
@@ -32,7 +34,7 @@ export function AddCompatibilityQuestionButton(props: {
onClick={() => setOpen(true)}
className="text-sm"
>
submit your own!
{t('answers.add.submit_own', 'submit your own!')}
</button>
<AddCompatibilityQuestionModal
open={open}
@@ -94,6 +96,7 @@ function CreateCompatibilityModalContent(props: {
setOpen: (open: boolean) => void
}) {
const { afterAddQuestion, setOpen } = props
const t = useT()
const [question, setQuestion] = useState('')
const [options, setOptions] = useState<string[]>(['', ''])
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: {
<Col className="w-full gap-4 main-font">
<Col className="gap-1">
<label>
Question<span className={'text-scarlet-500'}>*</span>
{t('answers.add.question_label', 'Question')}
<span className={'text-scarlet-500'}>*</span>
</label>
<ExpandingInput
maxLength={MAX_COMPATIBILITY_QUESTION_LENGTH}
@@ -162,7 +166,8 @@ function CreateCompatibilityModalContent(props: {
</Col>
<Col className="gap-1">
<label>
Options<span className={'text-scarlet-500'}>*</span>
{t('answers.add.options_label', 'Options')}
<span className={'text-scarlet-500'}>*</span>
</label>
<Col className="w-full gap-1">
{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: {
<Button onClick={addOption} color="gray-outline">
<Row className="items-center gap-1">
<PlusIcon className="h-4 w-4" />
Add Option
{t('answers.add.add_option', 'Add Option')}
</Row>
</Button>
</Col>
@@ -201,7 +206,7 @@ function CreateCompatibilityModalContent(props: {
setOpen(false)
}}
>
Cancel
{t('settings.action.cancel', 'Cancel')}
</Button>
<Button
loading={loading}
@@ -211,7 +216,7 @@ function CreateCompatibilityModalContent(props: {
}}
disabled={!optionsAreValid || !questionIsValid || !noRepeatOptions}
>
Submit & Answer
{t('answers.add.submit_and_answer', 'Submit & Answer')}
</Button>
</Row>
</Col>

View File

@@ -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' ? (
<Button onClick={() => setOpen(true)} color="none" className={'px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 hover:text-ink-900'}>
Answer{isCore && ' Core'} Questions{' '}
{t('answers.answer.cta', 'Answer{core} Questions', { core: isCore ? ' Core' : '' })}{' '}
<span className="text-primary-600 ml-2">
+{questionsToAnswer.length}
</span>
@@ -41,7 +43,7 @@ export function AnswerCompatibilityQuestionButton(props: {
onClick={() => setOpen(true)}
className="bg-ink-100 dark:bg-ink-300 text-ink-1000 hover:bg-ink-200 hover:dark:bg-ink-400 w-28 rounded-full px-2 py-0.5 text-xs transition-colors"
>
Answer yourself
{t('answers.answer.answer_yourself', 'Answer yourself')}
</button>
)}
<AnswerCompatibilityQuestionModal
@@ -59,11 +61,12 @@ export function AnswerCompatibilityQuestionButton(props: {
}
export function CompatibilityPageButton() {
const t = useT()
return (
<Link
href="/compatibility"
className="px-3 py-2 rounded-md border border-primary-600 text-ink-700 hover:bg-primary-50 flex items-center justify-center text-center"
>View List of Questions</Link>
>{t('answers.answer.view_list', 'View List of Questions')}</Link>
)
}
@@ -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 (
<>
<button
onClick={() => setOpen(true)}
className="text-ink-500 text-sm hover:underline"
>
Answer {skippedQuestions.length} skipped questions{' '}
{t('answers.answer.answer_skipped', 'Answer {n} skipped questions', { n: String(skippedQuestions.length) })}{' '}
</button>
<AnswerCompatibilityQuestionModal
open={open}

View File

@@ -18,6 +18,7 @@ import {track} from 'web/lib/service/analytics'
import {api} from 'web/lib/api'
import {filterKeys} from '../questions-form'
import toast from "react-hot-toast"
import {useT} from 'web/lib/locale'
export type CompatibilityAnswerSubmitType = Omit<
rowFor<'compatibility_answers'>,
@@ -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<CompatibilityAnswerSubmitType>(
(props.answer as CompatibilityAnswerSubmitType) ??
getEmptyAnswer(user.id, compatibilityQuestion.id)
@@ -175,7 +179,7 @@ export function AnswerCompatibilityQuestionContent(props: {
{shortenedPopularity && (
<Row className="text-ink-500 select-none items-center text-sm">
<Tooltip
text={`${shortenedPopularity} people have answered this question`}
text={t('answers.content.people_answered', '{count} people have answered this question', { count: String(shortenedPopularity) })}
>
{shortenedPopularity}
</Tooltip>
@@ -190,7 +194,7 @@ export function AnswerCompatibilityQuestionContent(props: {
)}
>
<Col className="gap-2">
<span className="text-ink-500 text-sm">Your answer</span>
<span className="text-ink-500 text-sm">{t('answers.content.your_answer', 'Your answer')}</span>
<SelectAnswer
value={answer.multiple_choice}
setValue={(choice) =>
@@ -200,7 +204,7 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
</Col>
<Col className="gap-2">
<span className="text-ink-500 text-sm">Answers you'll accept</span>
<span className="text-ink-500 text-sm">{t('answers.content.answers_you_accept', "Answers you'll accept")}</span>
<MultiSelectAnswers
values={answer.pref_choices ?? []}
setValue={(choice) =>
@@ -210,7 +214,7 @@ export function AnswerCompatibilityQuestionContent(props: {
/>
</Col>
<Col className="gap-2">
<span className="text-ink-500 text-sm">Importance</span>
<span className="text-ink-500 text-sm">{t('answers.content.importance', 'Importance')}</span>
<RadioToggleGroup
currentChoice={answer.importance ?? -1}
choicesMap={IMPORTANCE_CHOICES}
@@ -222,7 +226,7 @@ export function AnswerCompatibilityQuestionContent(props: {
</Col>
<Col className="-mt-6 gap-2">
<span className="text-ink-500 text-sm">
Your thoughts (optional, but recommended)
{t('answers.content.your_thoughts', 'Your thoughts (optional, but recommended)')}
</span>
<ExpandingInput
className={'w-full'}
@@ -259,7 +263,7 @@ export function AnswerCompatibilityQuestionContent(props: {
skipLoading && 'animate-pulse'
)}
>
Skip
{t('answers.content.skip', 'Skip')}
</button>
)}
<Button
@@ -284,7 +288,7 @@ export function AnswerCompatibilityQuestionContent(props: {
.finally(() => setLoading(false))
}}
>
{isLastQuestion ? 'Finish' : 'Next'}
{isLastQuestion ? t('answers.content.finish', 'Finish') : t('answers.content.next', 'Next')}
</Button>
</Row>
</Col>

View File

@@ -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: {
) : (
<XCircleIcon className="h-4 w-4" />
)}
{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) })}
</Row>
)}
</Row>

View File

@@ -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: {
<Col className="gap-4">
<Row className="flex-wrap items-center justify-between gap-x-6 gap-y-4">
<Row className={'gap-8'}>
<Subtitle>{`${
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
} Compatibility Prompts`}</Subtitle>
<Subtitle>
{isCurrentUser
? t('answers.display.your_prompts', 'Your Compatibility Prompts')
: t('answers.display.user_prompts', "{name}'s Compatibility Prompts", { name: shortenName(user.name) })}
</Subtitle>
{compatibilityScore &&
<CompatibleBadge compatibility={compatibilityScore} className={'mt-4 mr-4'}/>
}
@@ -194,10 +198,11 @@ export function CompatibilityQuestionsDisplay(props: {
</Row>
{answeredQuestions.length <= 0 ? (
<span className="text-ink-600 text-sm">
{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.")}</>
)}
</span>
) : (
@@ -206,11 +211,11 @@ export function CompatibilityQuestionsDisplay(props: {
<span className='custom-link'>
{otherQuestions.length < 1 ? (
<span className="text-ink-600 text-sm">
You've already answered all the compatibility questions
{t('answers.display.already_answered_all', "You've already answered all the compatibility questions—")}
</span>
) : (
<span className="text-ink-600 text-sm">
Answer more questions to increase your compatibility scoresor{' '}
{t('answers.display.answer_more', 'Answer more questions to increase your compatibility scores—or ')}
</span>
)}
<AddCompatibilityQuestionButton
@@ -233,7 +238,7 @@ export function CompatibilityQuestionsDisplay(props: {
)
})}
{shownAnswers.length === 0 && (
<div className="text-ink-500">None</div>
<div className="text-ink-500">{t('answers.display.none', 'None')}</div>
)}
</>
)}
@@ -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<boolean>(false)
const currentUser = useUser()
const currentProfile = useProfile()
const t = useT()
const [newAnswer, setNewAnswer] = useState<CompatibilityAnswerSubmitType | undefined>(props.answer)
@@ -408,12 +415,12 @@ export function CompatibilityAnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Edit',
name: t('answers.menu.edit', 'Edit'),
icon: <PencilIcon className="h-5 w-5"/>,
onClick: () => setEditOpen(true),
},
{
name: 'Delete',
name: t('answers.menu.delete', 'Delete'),
icon: <TrashIcon className="h-5 w-5"/>,
onClick: () => {
deleteCompatibilityAnswer(answer.id, user.id)
@@ -436,7 +443,7 @@ export function CompatibilityAnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Skip',
name: t('answers.menu.skip', 'Skip'),
icon: <TrashIcon className="h-5 w-5"/>,
onClick: () => {
submitCompatibilityAnswer(getEmptyAnswer(user.id, question.id))
@@ -469,9 +476,9 @@ export function CompatibilityAnswerBlock(props: {
{distinctPreferredAnswersText.length > 0 && (
<Col className="gap-2">
<div className="text-ink-800 text-sm">
{preferredDoesNotIncludeAnswerText
? 'Acceptable'
: 'Also acceptable'}
{preferredDoesNotIncludeAnswerText
? t('answers.display.acceptable', 'Acceptable')
: t('answers.display.also_acceptable', 'Also acceptable')}
</div>
<Row className="flex-wrap gap-2 mt-0">
{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')}
</button>
</>
)}
@@ -644,10 +651,10 @@ function CompatibilityDisplay(props: {
<Subtitle>{question.question}</Subtitle>
<Col className={clsx('w-full gap-1', SCROLLABLE_MODAL_CLASS)}>
<div className="text-ink-600 items-center gap-2">
{`${shortenName(user1.name)}'s preferred answers`}
{t('answers.modal.preferred_of_user', "{name}'s preferred answers", { name: shortenName(user1.name) })}
</div>
<div className="text-ink-500 text-sm">
{shortenName(user1.name)} marked this as{' '}
{t('answers.modal.user_marked', '{name} marked this as ', { name: shortenName(user1.name) })}
<span className="font-semibold">
<ImportanceDisplay importance={answer1.importance}/>
</span>
@@ -666,13 +673,14 @@ function CompatibilityDisplay(props: {
/>
<div className="text-ink-600 mt-6 items-center gap-2">
{`${
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) })}
</div>
<div className="text-ink-500 text-sm">
{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) })}
<span className="font-semibold">
<ImportanceDisplay importance={answer2.importance}/>
</span>

View File

@@ -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 (
<>
<Button color={'gray-outline'} onClick={() => setOpenModal(true)}>
<Row className="items-center gap-1">
<PlusIcon className="h-4 w-4"/>
Add Free Response
{t('answers.free.add_free_response', 'Add Free Response')}
</Row>
</Button>
<AddQuestionModal
@@ -63,6 +65,7 @@ function AddQuestionModal(props: {
null,
`selected-expanded-question-${user.id}}`
)
const t = useT()
return (
<Modal open={open} setOpen={setOpen}>
@@ -91,7 +94,7 @@ function AddQuestionModal(props: {
) : selectedQuestion == null ? (
<>
<div className="text-primary-600 w-full font-semibold">
Choose a question to answer
{t('answers.free.choose_question', 'Choose a question to answer')}
</div>
<Col className={SCROLLABLE_MODAL_CLASS}>
{addableQuestions.map((question) => {

View File

@@ -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 (
<Col className="gap-2">
<Row className={'w-full items-center justify-between gap-2'}>
<Subtitle>{`${
isCurrentUser ? 'Your' : shortenName(user.name) + `'s`
} Free Response`}</Subtitle>
<Subtitle>
{isCurrentUser
? t('answers.free.your_title', 'Your Free Response')
: t('answers.free.user_title', "{name}'s Free Response", { name: shortenName(user.name) })}
</Subtitle>
</Row>
<Col className="gap-2">
@@ -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<boolean>(false)
@@ -119,18 +124,18 @@ function AnswerBlock(props: {
<DropdownMenu
items={[
{
name: 'Edit',
name: t('answers.menu.edit', 'Edit'),
icon: <PencilIcon className="h-5 w-5" />,
onClick: () => setEdit(true),
},
{
name: 'Delete',
name: t('answers.menu.delete', 'Delete'),
icon: <XIcon className="h-5 w-5" />,
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: <TbMessage className="h-5 w-5" />,
onClick: () => setOtherAnswerModal(true),
},

View File

@@ -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: {
<Button color="indigo" onClick={() => Router.push('opinion-scale')}>
<Row className="items-center gap-1">
<BiTachometer className="h-5 w-5" />
Fill Opinion Scale
{t('answers.opinion.fill', 'Fill Opinion Scale')}
</Row>
</Button>
)
@@ -39,7 +41,7 @@ export function OpinionScale(props: {
return (
<Col className="gap-2">
<Row className={'w-full items-center justify-between gap-2'}>
<Subtitle>Opinion Scale</Subtitle>
<Subtitle>{t('answers.opinion.title', 'Opinion Scale')}</Subtitle>
{isCurrentUser && (
<Button
@@ -52,7 +54,7 @@ export function OpinionScale(props: {
}}
>
<PencilIcon className="mr-2 h-4 w-4" />
Edit
{t('answers.opinion.edit', 'Edit')}
</Button>
)}
</Row>

View File

@@ -9,7 +9,8 @@ export type I18nContextType = {
export const I18nContext = createContext<I18nContextType>({
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
}
}

View File

@@ -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"
}