From bda34dddd01f0c530878677eed8adc952aa08ba4 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sun, 25 Jan 2026 22:18:54 +0100 Subject: [PATCH] Add translation support for compatibility prompts --- backend/api/package.json | 2 +- .../api/src/get-compatibililty-questions.ts | 65 +++++++++++++------ .../compatibility_prompts_translations.sql | 44 +++++++++++++ common/src/api/schema.ts | 4 +- common/src/supabase/schema.ts | 34 +++++++++- web/hooks/use-questions.ts | 18 ++--- web/messages/fr.json | 2 +- 7 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 backend/supabase/compatibility_prompts_translations.sql diff --git a/backend/api/package.json b/backend/api/package.json index 34434910..47226faa 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -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", diff --git a/backend/api/src/get-compatibililty-questions.ts b/backend/api/src/get-compatibililty-questions.ts index fa601923..9433a7c3 100644 --- a/backend/api/src/get-compatibililty-questions.ts +++ b/backend/api/src/get-compatibililty-questions.ts @@ -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(array: T[]): T[] { const arr = [...array]; // copy to avoid mutating the original @@ -13,30 +13,57 @@ export function shuffle(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( diff --git a/backend/supabase/compatibility_prompts_translations.sql b/backend/supabase/compatibility_prompts_translations.sql new file mode 100644 index 00000000..7966530a --- /dev/null +++ b/backend/supabase/compatibility_prompts_translations.sql @@ -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); diff --git a/common/src/api/schema.ts b/common/src/api/schema.ts index 32266252..8f78fa34 100644 --- a/common/src/api/schema.ts +++ b/common/src/api/schema.ts @@ -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'> & { diff --git a/common/src/supabase/schema.ts b/common/src/supabase/schema.ts index c49afab4..46ed6a5f 100644 --- a/common/src/supabase/schema.ts +++ b/common/src/supabase/schema.ts @@ -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 diff --git a/web/hooks/use-questions.ts b/web/hooks/use-questions.ts index 1773b419..0cc836ee 100644 --- a/web/hooks/use-questions.ts +++ b/web/hooks/use-questions.ts @@ -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[]>([]) @@ -93,6 +94,7 @@ export const useFRQuestionsWithAnswerCount = () => { } export const useCompatibilityQuestionsWithAnswerCount = () => { + const {locale} = useLocale() const [compatibilityQuestions, setCompatibilityQuestions] = usePersistentInMemoryState( [], @@ -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, diff --git a/web/messages/fr.json b/web/messages/fr.json index 25dd0178..f8d976f1 100644 --- a/web/messages/fr.json +++ b/web/messages/fr.json @@ -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",