Add translation support for compatibility prompts

This commit is contained in:
MartinBraquet
2026-01-25 22:18:54 +01:00
parent ccb2eaaddf
commit bda34dddd0
7 changed files with 138 additions and 31 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@compass/api",
"description": "Backend API endpoints",
"version": "1.0.15",
"version": "1.1.0",
"private": true,
"scripts": {
"watch:serve": "tsx watch src/serve.ts",

View File

@@ -1,6 +1,6 @@
import { type APIHandler } from 'api/helpers/endpoint'
import { createSupabaseDirectClient } from 'shared/supabase/init'
import { Row } from 'common/supabase/utils'
import {type APIHandler} from 'api/helpers/endpoint'
import {createSupabaseDirectClient} from 'shared/supabase/init'
import {Row} from 'common/supabase/utils'
export function shuffle<T>(array: T[]): T[] {
const arr = [...array]; // copy to avoid mutating the original
@@ -13,30 +13,57 @@ export function shuffle<T>(array: T[]): T[] {
export const getCompatibilityQuestions: APIHandler<
'get-compatibility-questions'
> = async (_props, _auth) => {
> = async (props, _auth) => {
const {locale = 'en'} = props
const pg = createSupabaseDirectClient()
const questions = await pg.manyOrNone<
Row<'compatibility_prompts'> & { answer_count: number; score: number }
>(
`SELECT
compatibility_prompts.*,
COUNT(compatibility_answers.question_id) as answer_count,
AVG(POWER(compatibility_answers.importance + 1 + CASE WHEN compatibility_answers.explanation IS NULL THEN 1 ELSE 0 END, 2)) as score
FROM
compatibility_prompts
LEFT JOIN
compatibility_answers ON compatibility_prompts.id = compatibility_answers.question_id
WHERE
compatibility_prompts.answer_type = 'compatibility_multiple_choice'
GROUP BY
compatibility_prompts.id
ORDER BY
compatibility_prompts.importance_score
`
SELECT cp.id,
cp.answer_type,
cp.importance_score,
cp.created_time,
cp.creator_id,
cp.category,
-- locale-aware fields
COALESCE(cpt.question, cp.question) AS question,
COALESCE(cpt.multiple_choice_options, cp.multiple_choice_options) AS multiple_choice_options,
COUNT(ca.question_id) AS answer_count,
AVG(
POWER(
ca.importance + 1 +
CASE WHEN ca.explanation IS NULL THEN 1 ELSE 0 END,
2
)
) AS score
FROM compatibility_prompts cp
LEFT JOIN compatibility_answers ca
ON cp.id = ca.question_id
LEFT JOIN compatibility_prompts_translations cpt
ON cp.id = cpt.question_id
AND cpt.locale = $1
AND $1 <> 'en'
WHERE cp.answer_type = 'compatibility_multiple_choice'
GROUP BY cp.id,
cpt.question,
cpt.multiple_choice_options
ORDER BY cp.importance_score
`,
[]
[locale]
)
// console.debug({questions})
// const questions = shuffle(dbQuestions)
// console.debug(

View File

@@ -0,0 +1,44 @@
CREATE TABLE IF NOT EXISTS compatibility_prompts_translations
(
question_id BIGINT NOT NULL,
locale TEXT NOT NULL,
multiple_choice_options JSONB,
question TEXT NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL,
PRIMARY KEY (question_id, locale),
CONSTRAINT fk_question
FOREIGN KEY (question_id)
REFERENCES compatibility_prompts (id)
ON DELETE CASCADE
);
-- Add triggers for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS
$$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ language 'plpgsql';
CREATE TRIGGER update_compatibility_prompts_translations_updated_at
BEFORE UPDATE
ON compatibility_prompts_translations
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Add RLS policies
ALTER TABLE compatibility_prompts_translations
ENABLE ROW LEVEL SECURITY;
-- Public read access
CREATE POLICY "public read"
ON compatibility_prompts_translations
FOR SELECT
USING (true);
-- Indexes for better performance
CREATE INDEX IF NOT EXISTS idx_compatibility_prompts_translations_locale ON compatibility_prompts_translations (locale);
CREATE INDEX IF NOT EXISTS idx_compatibility_prompts_translations_question_id ON compatibility_prompts_translations (question_id);
CREATE INDEX IF NOT EXISTS idx_cpt_locale ON compatibility_prompts_translations (question_id, locale);

View File

@@ -345,7 +345,9 @@ export const API = (_apiTypeCheck = {
method: 'GET',
authed: true,
rateLimited: false,
props: z.object({}),
props: z.object({
locale: z.string().optional()
}),
returns: {} as {
status: 'success'
questions: (Row<'compatibility_prompts'> & {

View File

@@ -198,6 +198,38 @@ export type Database = {
},
]
}
compatibility_prompts_translations: {
Row: {
locale: string
multiple_choice_options: Json | null
question: string
question_id: number
updated_at: string
}
Insert: {
locale: string
multiple_choice_options?: Json | null
question: string
question_id: number
updated_at?: string
}
Update: {
locale?: string
multiple_choice_options?: Json | null
question?: string
question_id?: number
updated_at?: string
}
Relationships: [
{
foreignKeyName: 'fk_question'
columns: ['question_id']
isOneToOne: false
referencedRelation: 'compatibility_prompts'
referencedColumns: ['id']
},
]
}
compatibility_scores: {
Row: {
created_time: string
@@ -1301,8 +1333,8 @@ export type Database = {
}[]
}
is_admin:
| { Args: { user_id: string }; Returns: boolean }
| { Args: never; Returns: boolean }
| { Args: { user_id: string }; Returns: boolean }
millis_interval: {
Args: { end_millis: number; start_millis: number }
Returns: unknown

View File

@@ -1,15 +1,16 @@
import { sortBy } from 'lodash'
import { useEffect, useState } from 'react'
import { Row } from 'common/supabase/utils'
import {sortBy} from 'lodash'
import {useEffect, useState} from 'react'
import {Row} from 'common/supabase/utils'
import {
getAllQuestions,
getFRQuestionsWithAnswerCount,
getFreeResponseQuestions,
getFRQuestionsWithAnswerCount,
getUserAnswers,
getUserCompatibilityAnswers,
} from 'web/lib/supabase/questions'
import { usePersistentInMemoryState } from 'web/hooks/use-persistent-in-memory-state'
import { api } from 'web/lib/api'
import {usePersistentInMemoryState} from 'web/hooks/use-persistent-in-memory-state'
import {api} from 'web/lib/api'
import {useLocale} from "web/lib/locale";
export const useQuestions = () => {
const [questions, setQuestions] = useState<Row<'compatibility_prompts'>[]>([])
@@ -93,6 +94,7 @@ export const useFRQuestionsWithAnswerCount = () => {
}
export const useCompatibilityQuestionsWithAnswerCount = () => {
const {locale} = useLocale()
const [compatibilityQuestions, setCompatibilityQuestions] =
usePersistentInMemoryState<QuestionWithCountType[]>(
[],
@@ -100,14 +102,14 @@ export const useCompatibilityQuestionsWithAnswerCount = () => {
)
async function refreshCompatibilityQuestions() {
return api('get-compatibility-questions', {}).then((res) => {
return api('get-compatibility-questions', {locale}).then((res) => {
setCompatibilityQuestions(res.questions)
})
}
useEffect(() => {
refreshCompatibilityQuestions()
}, [])
}, [locale])
return {
refreshCompatibilityQuestions,

View File

@@ -71,7 +71,7 @@
"answers.add.submit_own": "Proposez la vôtre !",
"answers.answer.answer_skipped": "Répondre à {n} questions ignorées",
"answers.answer.answer_yourself": "Répondez vous-même",
"answers.answer.cta": "Répondre{core} aux questions",
"answers.answer.cta": "Répondre aux questions",
"answers.answer.view_list": "Voir la liste des questions",
"answers.compatible": "Compatible",
"answers.content.answers_you_accept": "Réponses que vous acceptez",