From 9422111235a4e1e88f9d05f73e63f59100808b5f Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Mon, 13 Apr 2026 15:00:29 +0200 Subject: [PATCH 1/4] feat(frontend): fetch non-english locales from api --- cloudflare_workers/api/index.ts | 2 + src/constants/locales.ts | 45 ++++++++++ src/modules/i18n.ts | 90 ++++++++++--------- .../_backend/public/translations/index.ts | 17 ++++ .../_backend/public/translations/messages.ts | 40 +++++++++ supabase/functions/translations/index.ts | 11 +++ tests/i18n-loader.unit.test.ts | 59 ++++++++++++ tests/translations.test.ts | 41 +++++++++ 8 files changed, 263 insertions(+), 42 deletions(-) create mode 100644 src/constants/locales.ts create mode 100644 supabase/functions/_backend/public/translations/index.ts create mode 100644 supabase/functions/_backend/public/translations/messages.ts create mode 100644 supabase/functions/translations/index.ts create mode 100644 tests/i18n-loader.unit.test.ts create mode 100644 tests/translations.test.ts diff --git a/cloudflare_workers/api/index.ts b/cloudflare_workers/api/index.ts index 52e6687ed2..30cdc4d285 100644 --- a/cloudflare_workers/api/index.ts +++ b/cloudflare_workers/api/index.ts @@ -41,6 +41,7 @@ import { app as ok } from '../../supabase/functions/_backend/public/ok.ts' import { app as organization } from '../../supabase/functions/_backend/public/organization/index.ts' import { app as replication } from '../../supabase/functions/_backend/public/replication.ts' import { app as statistics } from '../../supabase/functions/_backend/public/statistics/index.ts' +import { app as translations } from '../../supabase/functions/_backend/public/translations/index.ts' import { app as webhooks } from '../../supabase/functions/_backend/public/webhooks/index.ts' import { app as credit_usage_alerts } from '../../supabase/functions/_backend/triggers/credit_usage_alerts.ts' import { app as cron_clean_orphan_images } from '../../supabase/functions/_backend/triggers/cron_clean_orphan_images.ts' @@ -88,6 +89,7 @@ app.route('/app', appEndpoint) app.route('/build', build) app.route('/replication', replication) app.route('/check_cpu_usage', check_cpu_usage) +app.route('/translations', translations) // Private API const functionNamePrivate = 'private' diff --git a/src/constants/locales.ts b/src/constants/locales.ts new file mode 100644 index 0000000000..9e63b09853 --- /dev/null +++ b/src/constants/locales.ts @@ -0,0 +1,45 @@ +export const languages = { + 'de': 'Deutsch', + 'en': 'English', + 'es': 'Español', + 'id': 'Bahasa Indonesia', + 'it': 'Italiano', + 'fr': 'Français', + 'ja': '日本語', + 'ko': '한국어', + 'pl': 'Polski', + 'pt-br': 'Português (Brasil)', + 'ru': 'Русский', + 'tr': 'Türkçe', + 'vi': 'Tiếng Việt', + 'zh-cn': '简体中文', + 'hi': 'हिन्दी', +} as const + +export type SupportedLocale = keyof typeof languages + +export const defaultLocale: SupportedLocale = 'en' +export const supportedLocales = Object.keys(languages) as SupportedLocale[] + +export function isSupportedLocale(locale: string): locale is SupportedLocale { + return locale in languages +} + +export function resolveLocale(locale?: string | null): SupportedLocale | null { + if (!locale) + return null + + const normalizedLocale = locale.toLowerCase() + if (isSupportedLocale(normalizedLocale)) + return normalizedLocale + + const baseLocale = normalizedLocale.split('-')[0] + if (isSupportedLocale(baseLocale)) + return baseLocale + + return null +} + +export function normalizeLocale(locale?: string | null): SupportedLocale { + return resolveLocale(locale) ?? defaultLocale +} diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts index 46fae89757..09d1fd3496 100644 --- a/src/modules/i18n.ts +++ b/src/modules/i18n.ts @@ -1,43 +1,26 @@ import type { Locale } from 'vue-i18n' +import type { SupportedLocale } from '../constants/locales' import type { UserModule } from '~/types' import { createI18n } from 'vue-i18n' +import { defaultApiHost } from '~/services/supabase' +import enMessages from '../../messages/en.json' +import { defaultLocale, languages, normalizeLocale, supportedLocales } from '../constants/locales' -// Import i18n resources -// https://vitejs.dev/guide/features.html#glob-import -// -// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite export const i18n = createI18n({ legacy: false, - fallbackLocale: 'en', - locale: '', - messages: {}, + fallbackLocale: defaultLocale, + locale: defaultLocale, + messages: { + [defaultLocale]: enMessages, + }, }) -const localesMap = Object.fromEntries( - Object.entries(import.meta.glob('../../messages/*.json')) - .map(([path, loadLocale]) => [/([\w-]*)\.json$/.exec(path)?.[1], loadLocale]), -) as Record Promise<{ default: Record }>> +type MessageDictionary = Record -export const availableLocales = Object.keys(localesMap) -export const languages = { - 'de': 'Deutsch', - 'en': 'English', - 'es': 'Español', - 'id': 'Bahasa Indonesia', - 'it': 'Italiano', - 'fr': 'Français', - 'ja': '日本語', - 'ko': '한국어', - 'pl': 'Polski', - 'pt-br': 'Português (Brasil)', - 'ru': 'Русский', - 'tr': 'Türkçe', - 'vi': 'Tiếng Việt', - 'zh-cn': '简体中文', - 'hi': 'हिन्दी', -} +export const availableLocales = supportedLocales +export { languages } -const loadedLanguages: string[] = [] +const loadedLanguages: SupportedLocale[] = [defaultLocale] function setI18nLanguage(lang: Locale) { i18n.global.locale.value = lang as any @@ -47,26 +30,49 @@ function setI18nLanguage(lang: Locale) { return lang } +async function loadLocaleMessages(locale: SupportedLocale): Promise { + if (locale === defaultLocale) + return enMessages + + const response = await fetch(`${defaultApiHost}/translations/${locale}`, { + headers: { + Accept: 'application/json', + }, + }) + + if (!response.ok) + throw new Error(`Failed to load locale "${locale}" (HTTP ${response.status})`) + + return await response.json() as MessageDictionary +} + export async function loadLanguageAsync(lang: string): Promise { + const locale = normalizeLocale(lang) + // If the same language - if (i18n.global.locale.value === lang) - return setI18nLanguage(lang) + if (i18n.global.locale.value === locale) + return setI18nLanguage(locale) // If the language was already loaded - if (loadedLanguages.includes(lang)) - return setI18nLanguage(lang) + if (loadedLanguages.includes(locale)) + return setI18nLanguage(locale) + + const fallbackLocale = normalizeLocale(i18n.global.locale.value) - // If the language hasn't been loaded yet - const messages = await localesMap[lang]() - i18n.global.setLocaleMessage(lang, messages.default) - loadedLanguages.push(lang) - return setI18nLanguage(lang) + try { + const messages = await loadLocaleMessages(locale) + i18n.global.setLocaleMessage(locale, messages) + loadedLanguages.push(locale) + return setI18nLanguage(locale) + } + catch (error) { + console.error('Failed to load locale messages', { locale, error }) + return setI18nLanguage(fallbackLocale) + } } export const install: UserModule = ({ app }) => { app.use(i18n) - let lang = localStorage.getItem('lang') ?? window.navigator.language.split('-')[0] - if (!(lang in languages)) - lang = 'en' + const lang = normalizeLocale(localStorage.getItem('lang') ?? window.navigator.language) loadLanguageAsync(lang) } diff --git a/supabase/functions/_backend/public/translations/index.ts b/supabase/functions/_backend/public/translations/index.ts new file mode 100644 index 0000000000..ef8310a803 --- /dev/null +++ b/supabase/functions/_backend/public/translations/index.ts @@ -0,0 +1,17 @@ +import { resolveLocale } from '../../../../../src/constants/locales.ts' +import { honoFactory, quickError, useCors } from '../../utils/hono.ts' +import { getLocaleMessages } from './messages.ts' + +export const app = honoFactory.createApp() + +app.use('*', useCors) + +app.get('/:locale', (c) => { + const locale = resolveLocale(c.req.param('locale')) + if (!locale) + return quickError(404, 'unsupported_locale', 'Unsupported locale') + + c.header('Cache-Control', 'public, max-age=300, s-maxage=86400') + c.header('Content-Language', locale) + return c.json(getLocaleMessages(locale)) +}) diff --git a/supabase/functions/_backend/public/translations/messages.ts b/supabase/functions/_backend/public/translations/messages.ts new file mode 100644 index 0000000000..b26aa2ec4e --- /dev/null +++ b/supabase/functions/_backend/public/translations/messages.ts @@ -0,0 +1,40 @@ +import type { SupportedLocale } from '../../../../../src/constants/locales.ts' +import deMessages from '../../../../../messages/de.json' with { type: 'json' } +import enMessages from '../../../../../messages/en.json' with { type: 'json' } +import esMessages from '../../../../../messages/es.json' with { type: 'json' } +import frMessages from '../../../../../messages/fr.json' with { type: 'json' } +import hiMessages from '../../../../../messages/hi.json' with { type: 'json' } +import idMessages from '../../../../../messages/id.json' with { type: 'json' } +import itMessages from '../../../../../messages/it.json' with { type: 'json' } +import jaMessages from '../../../../../messages/ja.json' with { type: 'json' } +import koMessages from '../../../../../messages/ko.json' with { type: 'json' } +import plMessages from '../../../../../messages/pl.json' with { type: 'json' } +import ptBrMessages from '../../../../../messages/pt-br.json' with { type: 'json' } +import ruMessages from '../../../../../messages/ru.json' with { type: 'json' } +import trMessages from '../../../../../messages/tr.json' with { type: 'json' } +import viMessages from '../../../../../messages/vi.json' with { type: 'json' } +import zhCnMessages from '../../../../../messages/zh-cn.json' with { type: 'json' } + +type MessageDictionary = Record + +const localeMessages: Record = { + 'de': deMessages, + 'en': enMessages, + 'es': esMessages, + 'fr': frMessages, + 'hi': hiMessages, + 'id': idMessages, + 'it': itMessages, + 'ja': jaMessages, + 'ko': koMessages, + 'pl': plMessages, + 'pt-br': ptBrMessages, + 'ru': ruMessages, + 'tr': trMessages, + 'vi': viMessages, + 'zh-cn': zhCnMessages, +} + +export function getLocaleMessages(locale: SupportedLocale): MessageDictionary { + return localeMessages[locale] +} diff --git a/supabase/functions/translations/index.ts b/supabase/functions/translations/index.ts new file mode 100644 index 0000000000..4ade4a28e7 --- /dev/null +++ b/supabase/functions/translations/index.ts @@ -0,0 +1,11 @@ +import { app } from '../_backend/public/translations/index.ts' +import { createAllCatch, createHono } from '../_backend/utils/hono.ts' +import { version } from '../_backend/utils/version.ts' + +const functionName = 'translations' +const appGlobal = createHono(functionName, version) + +appGlobal.route('/', app) +createAllCatch(appGlobal, functionName) + +Deno.serve(appGlobal.fetch) diff --git a/tests/i18n-loader.unit.test.ts b/tests/i18n-loader.unit.test.ts new file mode 100644 index 0000000000..c02de8bf32 --- /dev/null +++ b/tests/i18n-loader.unit.test.ts @@ -0,0 +1,59 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('~/services/supabase', () => ({ + defaultApiHost: 'https://api.capgo.test', +})) + +function createStorageMock() { + const store = new Map() + + return { + getItem: vi.fn((key: string) => store.get(key) ?? null), + setItem: vi.fn((key: string, value: string) => { + store.set(key, value) + }), + removeItem: vi.fn((key: string) => { + store.delete(key) + }), + } +} + +describe('i18n locale loader', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.resetModules() + vi.unstubAllGlobals() + + vi.stubGlobal('fetch', vi.fn()) + vi.stubGlobal('localStorage', createStorageMock()) + }) + + it('keeps English in the frontend bundle without fetching it from the backend', async () => { + const { i18n, loadLanguageAsync } = await import('../src/modules/i18n.ts') + + await loadLanguageAsync('en') + + expect(fetch).not.toHaveBeenCalled() + expect(i18n.global.locale.value).toBe('en') + expect(i18n.global.t('accept-invitation')).toBe('Accept Invitation') + }) + + it('falls back to the current locale when backend locale loading fails', async () => { + vi.mocked(fetch).mockResolvedValue({ + ok: false, + status: 500, + } as Response) + + const { i18n, loadLanguageAsync } = await import('../src/modules/i18n.ts') + + await loadLanguageAsync('fr') + + expect(fetch).toHaveBeenCalledWith('https://api.capgo.test/translations/fr', { + headers: { + Accept: 'application/json', + }, + }) + expect(i18n.global.locale.value).toBe('en') + expect(i18n.global.t('accept-invitation')).toBe('Accept Invitation') + }) +}) diff --git a/tests/translations.test.ts b/tests/translations.test.ts new file mode 100644 index 0000000000..7b24ba574a --- /dev/null +++ b/tests/translations.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' + +describe('[GET] /translations/:locale', () => { + it.concurrent('returns the requested locale payload with cache headers', async () => { + const { app: translations } = await import('../supabase/functions/_backend/public/translations/index.ts') + const { createAllCatch, createHono } = await import('../supabase/functions/_backend/utils/hono.ts') + const { version } = await import('../supabase/functions/_backend/utils/version.ts') + + const app = createHono('translations', version) + app.route('/', translations) + createAllCatch(app, 'translations') + + const response = await app.fetch(new Request('http://localhost/fr')) + + expect(response.status).toBe(200) + expect(response.headers.get('access-control-allow-origin')).toBe('*') + expect(response.headers.get('cache-control')).toBe('public, max-age=300, s-maxage=86400') + expect(response.headers.get('content-language')).toBe('fr') + + const data = await response.json() as Record + expect(data['accept-invitation']).toBe('Accepter l\'invitation') + }) + + it.concurrent('rejects unsupported locales', async () => { + const { app: translations } = await import('../supabase/functions/_backend/public/translations/index.ts') + const { createAllCatch, createHono } = await import('../supabase/functions/_backend/utils/hono.ts') + const { version } = await import('../supabase/functions/_backend/utils/version.ts') + + const app = createHono('translations', version) + app.route('/', translations) + createAllCatch(app, 'translations') + + const response = await app.fetch(new Request('http://localhost/klingon')) + + expect(response.status).toBe(404) + await expect(response.json()).resolves.toMatchObject({ + error: 'unsupported_locale', + message: 'Unsupported locale', + }) + }) +}) From b44e0ae184108c721779ba3c4155f0873a39c9eb Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Mon, 13 Apr 2026 16:39:09 +0200 Subject: [PATCH 2/4] fix(frontend): handle locale loader edge cases --- src/constants/locales.ts | 2 +- src/modules/i18n.ts | 23 ++++++--- .../_backend/public/translations/index.ts | 13 ++++- tests/i18n-loader.unit.test.ts | 47 +++++++++++++++++++ vitest.config.ts | 6 +++ 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/constants/locales.ts b/src/constants/locales.ts index 9e63b09853..c8b34ccdf0 100644 --- a/src/constants/locales.ts +++ b/src/constants/locales.ts @@ -22,7 +22,7 @@ export const defaultLocale: SupportedLocale = 'en' export const supportedLocales = Object.keys(languages) as SupportedLocale[] export function isSupportedLocale(locale: string): locale is SupportedLocale { - return locale in languages + return Object.prototype.hasOwnProperty.call(languages, locale) } export function resolveLocale(locale?: string | null): SupportedLocale | null { diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts index 09d1fd3496..40877f4b30 100644 --- a/src/modules/i18n.ts +++ b/src/modules/i18n.ts @@ -1,10 +1,10 @@ import type { Locale } from 'vue-i18n' -import type { SupportedLocale } from '../constants/locales' +import type { SupportedLocale } from '~/constants/locales' import type { UserModule } from '~/types' import { createI18n } from 'vue-i18n' +import { defaultLocale, languages, normalizeLocale, supportedLocales } from '~/constants/locales' import { defaultApiHost } from '~/services/supabase' import enMessages from '../../messages/en.json' -import { defaultLocale, languages, normalizeLocale, supportedLocales } from '../constants/locales' export const i18n = createI18n({ legacy: false, @@ -21,6 +21,7 @@ export const availableLocales = supportedLocales export { languages } const loadedLanguages: SupportedLocale[] = [defaultLocale] +let latestLocaleRequestId = 0 function setI18nLanguage(lang: Locale) { i18n.global.locale.value = lang as any @@ -34,7 +35,11 @@ async function loadLocaleMessages(locale: SupportedLocale): Promise { const locale = normalizeLocale(lang) + const requestId = ++latestLocaleRequestId // If the same language if (i18n.global.locale.value === locale) - return setI18nLanguage(locale) + return requestId === latestLocaleRequestId ? setI18nLanguage(locale) : i18n.global.locale.value // If the language was already loaded if (loadedLanguages.includes(locale)) - return setI18nLanguage(locale) + return requestId === latestLocaleRequestId ? setI18nLanguage(locale) : i18n.global.locale.value const fallbackLocale = normalizeLocale(i18n.global.locale.value) try { const messages = await loadLocaleMessages(locale) i18n.global.setLocaleMessage(locale, messages) - loadedLanguages.push(locale) + if (!loadedLanguages.includes(locale)) + loadedLanguages.push(locale) + if (requestId !== latestLocaleRequestId) + return i18n.global.locale.value return setI18nLanguage(locale) } catch (error) { console.error('Failed to load locale messages', { locale, error }) + if (requestId !== latestLocaleRequestId) + return i18n.global.locale.value return setI18nLanguage(fallbackLocale) } } diff --git a/supabase/functions/_backend/public/translations/index.ts b/supabase/functions/_backend/public/translations/index.ts index ef8310a803..1ede11271b 100644 --- a/supabase/functions/_backend/public/translations/index.ts +++ b/supabase/functions/_backend/public/translations/index.ts @@ -1,5 +1,6 @@ import { resolveLocale } from '../../../../../src/constants/locales.ts' import { honoFactory, quickError, useCors } from '../../utils/hono.ts' +import { cloudlog } from '../../utils/logging.ts' import { getLocaleMessages } from './messages.ts' export const app = honoFactory.createApp() @@ -7,11 +8,19 @@ export const app = honoFactory.createApp() app.use('*', useCors) app.get('/:locale', (c) => { - const locale = resolveLocale(c.req.param('locale')) - if (!locale) + const requestId = c.get('requestId') + const requestedLocale = c.req.param('locale') + const locale = resolveLocale(requestedLocale) + + cloudlog({ requestId, message: 'translations request', requestedLocale, locale }) + + if (!locale) { + cloudlog({ requestId, message: 'translations unsupported locale', requestedLocale }) return quickError(404, 'unsupported_locale', 'Unsupported locale') + } c.header('Cache-Control', 'public, max-age=300, s-maxage=86400') c.header('Content-Language', locale) + cloudlog({ requestId, message: 'translations response', locale }) return c.json(getLocaleMessages(locale)) }) diff --git a/tests/i18n-loader.unit.test.ts b/tests/i18n-loader.unit.test.ts index c02de8bf32..9bd1560082 100644 --- a/tests/i18n-loader.unit.test.ts +++ b/tests/i18n-loader.unit.test.ts @@ -56,4 +56,51 @@ describe('i18n locale loader', () => { expect(i18n.global.locale.value).toBe('en') expect(i18n.global.t('accept-invitation')).toBe('Accept Invitation') }) + + it('keeps the latest locale selection when async fetches resolve out of order', async () => { + let resolveFrench: ((value: Response) => void) | undefined + let resolveGerman: ((value: Response) => void) | undefined + + vi.mocked(fetch).mockImplementation((url) => { + const requestUrl = String(url) + + if (requestUrl.endsWith('/translations/fr')) { + return new Promise((resolve) => { + resolveFrench = resolve + }) + } + + if (requestUrl.endsWith('/translations/de')) { + return new Promise((resolve) => { + resolveGerman = resolve + }) + } + + throw new Error(`Unexpected locale request: ${requestUrl}`) + }) + + const { i18n, loadLanguageAsync } = await import('../src/modules/i18n.ts') + + const frenchLoad = loadLanguageAsync('fr') + const germanLoad = loadLanguageAsync('de') + + resolveGerman?.({ + ok: true, + json: vi.fn().mockResolvedValue({ + 'accept-invitation': 'Einladung annehmen', + }), + } as unknown as Response) + await germanLoad + + resolveFrench?.({ + ok: true, + json: vi.fn().mockResolvedValue({ + 'accept-invitation': 'Accepter l\'invitation', + }), + } as unknown as Response) + await frenchLoad + + expect(i18n.global.locale.value).toBe('de') + expect(i18n.global.t('accept-invitation')).toBe('Einladung annehmen') + }) }) diff --git a/vitest.config.ts b/vitest.config.ts index b7ed3c6302..18c9deb443 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,14 @@ +import path from 'node:path' import { cwd } from 'node:process' import { loadEnv } from 'vite' import { defineConfig } from 'vitest/config' export default defineConfig(({ mode }) => ({ + resolve: { + alias: { + '~/': `${path.resolve(__dirname, 'src')}/`, + }, + }, test: { include: ['tests/*.test.ts'], environment: 'node', From 0ce278ba264d5833b65afbfe2fcdeefbdb86caf2 Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Mon, 13 Apr 2026 16:50:40 +0200 Subject: [PATCH 3/4] refactor(test): drop i18n vitest alias --- src/modules/i18n.ts | 4 ++-- vitest.config.ts | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts index 40877f4b30..eae5a55a53 100644 --- a/src/modules/i18n.ts +++ b/src/modules/i18n.ts @@ -1,10 +1,10 @@ import type { Locale } from 'vue-i18n' -import type { SupportedLocale } from '~/constants/locales' +import type { SupportedLocale } from '../constants/locales' import type { UserModule } from '~/types' import { createI18n } from 'vue-i18n' -import { defaultLocale, languages, normalizeLocale, supportedLocales } from '~/constants/locales' import { defaultApiHost } from '~/services/supabase' import enMessages from '../../messages/en.json' +import { defaultLocale, languages, normalizeLocale, supportedLocales } from '../constants/locales' export const i18n = createI18n({ legacy: false, diff --git a/vitest.config.ts b/vitest.config.ts index 18c9deb443..b7ed3c6302 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,14 +1,8 @@ -import path from 'node:path' import { cwd } from 'node:process' import { loadEnv } from 'vite' import { defineConfig } from 'vitest/config' export default defineConfig(({ mode }) => ({ - resolve: { - alias: { - '~/': `${path.resolve(__dirname, 'src')}/`, - }, - }, test: { include: ['tests/*.test.ts'], environment: 'node', From 3da6bedf62c57596656184d7513981a197fa55df Mon Sep 17 00:00:00 2001 From: Martin Donadieu Date: Mon, 13 Apr 2026 16:56:23 +0200 Subject: [PATCH 4/4] refactor(api): lazy load translation payloads --- .../_backend/public/translations/index.ts | 4 +- .../_backend/public/translations/messages.ts | 62 +++++++++---------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/supabase/functions/_backend/public/translations/index.ts b/supabase/functions/_backend/public/translations/index.ts index 1ede11271b..f5a2ec8388 100644 --- a/supabase/functions/_backend/public/translations/index.ts +++ b/supabase/functions/_backend/public/translations/index.ts @@ -7,7 +7,7 @@ export const app = honoFactory.createApp() app.use('*', useCors) -app.get('/:locale', (c) => { +app.get('/:locale', async (c) => { const requestId = c.get('requestId') const requestedLocale = c.req.param('locale') const locale = resolveLocale(requestedLocale) @@ -22,5 +22,5 @@ app.get('/:locale', (c) => { c.header('Cache-Control', 'public, max-age=300, s-maxage=86400') c.header('Content-Language', locale) cloudlog({ requestId, message: 'translations response', locale }) - return c.json(getLocaleMessages(locale)) + return c.json(await getLocaleMessages(locale)) }) diff --git a/supabase/functions/_backend/public/translations/messages.ts b/supabase/functions/_backend/public/translations/messages.ts index b26aa2ec4e..b37c43e50d 100644 --- a/supabase/functions/_backend/public/translations/messages.ts +++ b/supabase/functions/_backend/public/translations/messages.ts @@ -1,40 +1,36 @@ import type { SupportedLocale } from '../../../../../src/constants/locales.ts' -import deMessages from '../../../../../messages/de.json' with { type: 'json' } -import enMessages from '../../../../../messages/en.json' with { type: 'json' } -import esMessages from '../../../../../messages/es.json' with { type: 'json' } -import frMessages from '../../../../../messages/fr.json' with { type: 'json' } -import hiMessages from '../../../../../messages/hi.json' with { type: 'json' } -import idMessages from '../../../../../messages/id.json' with { type: 'json' } -import itMessages from '../../../../../messages/it.json' with { type: 'json' } -import jaMessages from '../../../../../messages/ja.json' with { type: 'json' } -import koMessages from '../../../../../messages/ko.json' with { type: 'json' } -import plMessages from '../../../../../messages/pl.json' with { type: 'json' } -import ptBrMessages from '../../../../../messages/pt-br.json' with { type: 'json' } -import ruMessages from '../../../../../messages/ru.json' with { type: 'json' } -import trMessages from '../../../../../messages/tr.json' with { type: 'json' } -import viMessages from '../../../../../messages/vi.json' with { type: 'json' } -import zhCnMessages from '../../../../../messages/zh-cn.json' with { type: 'json' } type MessageDictionary = Record +interface MessageModule { + default: MessageDictionary +} + +const localeMessages = new Map() -const localeMessages: Record = { - 'de': deMessages, - 'en': enMessages, - 'es': esMessages, - 'fr': frMessages, - 'hi': hiMessages, - 'id': idMessages, - 'it': itMessages, - 'ja': jaMessages, - 'ko': koMessages, - 'pl': plMessages, - 'pt-br': ptBrMessages, - 'ru': ruMessages, - 'tr': trMessages, - 'vi': viMessages, - 'zh-cn': zhCnMessages, +const messageLoaders: Record Promise> = { + 'de': () => import('../../../../../messages/de.json', { with: { type: 'json' } }), + 'en': () => import('../../../../../messages/en.json', { with: { type: 'json' } }), + 'es': () => import('../../../../../messages/es.json', { with: { type: 'json' } }), + 'fr': () => import('../../../../../messages/fr.json', { with: { type: 'json' } }), + 'hi': () => import('../../../../../messages/hi.json', { with: { type: 'json' } }), + 'id': () => import('../../../../../messages/id.json', { with: { type: 'json' } }), + 'it': () => import('../../../../../messages/it.json', { with: { type: 'json' } }), + 'ja': () => import('../../../../../messages/ja.json', { with: { type: 'json' } }), + 'ko': () => import('../../../../../messages/ko.json', { with: { type: 'json' } }), + 'pl': () => import('../../../../../messages/pl.json', { with: { type: 'json' } }), + 'pt-br': () => import('../../../../../messages/pt-br.json', { with: { type: 'json' } }), + 'ru': () => import('../../../../../messages/ru.json', { with: { type: 'json' } }), + 'tr': () => import('../../../../../messages/tr.json', { with: { type: 'json' } }), + 'vi': () => import('../../../../../messages/vi.json', { with: { type: 'json' } }), + 'zh-cn': () => import('../../../../../messages/zh-cn.json', { with: { type: 'json' } }), } -export function getLocaleMessages(locale: SupportedLocale): MessageDictionary { - return localeMessages[locale] +export async function getLocaleMessages(locale: SupportedLocale): Promise { + const cachedMessages = localeMessages.get(locale) + if (cachedMessages) + return cachedMessages + + const { default: messages } = await messageLoaders[locale]() + localeMessages.set(locale, messages) + return messages }