Supabase

Profile language

Store the admin language of each user in the database.

This feature stores the admin language of each user in the database. It plugs into the i18n system of core through a locale driver.

The data

profiles.locale is text and can be null. Null means Automatic: the user has not chosen a language, so the admin uses the browser language.

The check profiles_locale_format accepts any code that looks like en or pt-BR. It does not list the allowed languages, so a new language needs no migration. The route checks the code against runtimeConfig.public.plutoI18n.adminLocales.

The migration is 005_profile_locale.sql.

Why a function writes the column

public.profiles has no update policy. A policy that lets a user update their own row would also let them write is_admin.

So the function public.set_my_locale(new_locale text) is the only writer.

  • It is security definer with an empty search_path.
  • It updates the row where id = auth.uid() and sets only locale and updated_at.
  • The migration revokes execute from public and anon, and grants it to authenticated.
  • The migration adds no RLS policy.

A user reads their own row through the existing select policy.

Routes

GET /api/_pluto/supabase/me/locale returns { locale: string | null } for the calling user.

  • A signed-out caller gets { locale: null }.
  • If the column does not exist yet, the route logs the error and returns { locale: null }.

PUT /api/_pluto/supabase/me/locale takes { locale: string | null }.

CaseAnswer
Signed out401 with the key supabase.errors.notLoggedIn
Body does not match400
Locale not in adminLocales400 with the key supabase.errors.localeUnsupported
The function set_my_locale does not exist409 with the key supabase.profile.migrationPending
Any other error500 with the key supabase.errors.localeSave
Success{ locale }

Every failure is an HTTP error with an i18n key. It is not a 200 with success: false.

The client

The layer registers a localeDriver that calls the two routes. If a save fails, the language card on the Settings page shows the reason in a toast. The switch stays on the device, because core keeps it in a cookie.

Core's pluto-auth-sync.client.ts loads the stored language when a session appears. The plugin is client-only. A server $fetch does not forward the user cookies, so a server run would read no locale and clear the cookie.

A site that did not apply migration 005

The language card still works, and the switch is kept in the cookie. The save returns 409, and the toast tells the admin to apply the pending migrations on /admin/migrations.