diff --git a/config/context_processors.py b/config/context_processors.py index da5e98a752..8f4ae91dbb 100644 --- a/config/context_processors.py +++ b/config/context_processors.py @@ -1,17 +1,54 @@ from django.conf import settings +from django.templatetags.static import static + from mathesar.utils.frontend import get_manifest_data def frontend_settings(request): + manifest_data = get_manifest_data() + development_mode = settings.MATHESAR_MODE == 'DEVELOPMENT' + + i18n_settings = get_i18n_settings(manifest_data, development_mode) frontend_settings = { - 'development_mode': settings.MATHESAR_MODE == 'DEVELOPMENT', - 'manifest_data': get_manifest_data(), + 'development_mode': development_mode, + 'manifest_data': manifest_data, 'live_demo_mode': getattr(settings, 'MATHESAR_LIVE_DEMO', False), 'live_demo_username': getattr(settings, 'MATHESAR_LIVE_DEMO_USERNAME', None), 'live_demo_password': getattr(settings, 'MATHESAR_LIVE_DEMO_PASSWORD', None), + **i18n_settings } # Only include development URL if we're in development mode. if frontend_settings['development_mode'] is True: frontend_settings['client_dev_url'] = settings.MATHESAR_CLIENT_DEV_URL + return frontend_settings + + +def get_i18n_settings(manifest_data, development_mode): + """ + Hard coding this for now + but will be taken from users model + and cookies later on + """ + display_language = 'en' + fallback_language = 'en' + + client_dev_url = settings.MATHESAR_CLIENT_DEV_URL + + if development_mode is True: + module_translations_file_path = f'{client_dev_url}/src/i18n/{display_language}/index.ts' + legacy_translations_file_path = f'{client_dev_url}/src/i18n/{display_language}/index.ts' + else: + try: + module_translations_file_path = static(manifest_data[display_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{display_language}-legacy"]["file"]) + except KeyError: + module_translations_file_path = static(manifest_data[fallback_language]["file"]) + legacy_translations_file_path = static(manifest_data[f"{fallback_language}-legacy"]["file"]) + + return { + 'module_translations_file_path': module_translations_file_path, + 'legacy_translations_file_path': legacy_translations_file_path, + 'display_language': display_language + } diff --git a/config/settings/common_settings.py b/config/settings/common_settings.py index 3889a27cc2..af4a9297de 100644 --- a/config/settings/common_settings.py +++ b/config/settings/common_settings.py @@ -15,6 +15,7 @@ from decouple import Csv, config as decouple_config from dj_database_url import parse as db_url +from django.utils.translation import gettext_lazy # We use a 'tuple' with pipes as delimiters as decople naively splits the global @@ -254,4 +255,9 @@ def pipe_delim(pipe_string): # List of Template names that contains additional script tags to be added to the base template BASE_TEMPLATE_ADDITIONAL_SCRIPT_TEMPLATES = [] +# i18n +LANGUAGES = [ + ('en', gettext_lazy('English')), + ('ja', gettext_lazy('Japanese')), +] SALT_KEY = SECRET_KEY diff --git a/mathesar/templates/mathesar/index.html b/mathesar/templates/mathesar/index.html index e26130095d..30e8593603 100644 --- a/mathesar/templates/mathesar/index.html +++ b/mathesar/templates/mathesar/index.html @@ -4,9 +4,11 @@ {% block title %}Home{% endblock %} {% block styles %} - {% if not development_mode %} {% for css_file in manifest_data.module_css %} - - {% endfor %} {% endif %} + {% if not development_mode %} + {% for css_file in manifest_data.module_css %} + + {% endfor %} + {% endif %} {% endblock %} {% block scripts %} @@ -17,6 +19,8 @@ {% endfor %} {% endif %} + + {% if development_mode %} @@ -52,12 +56,22 @@ > + + {% endif %} {% endblock %} diff --git a/mathesar/utils/frontend.py b/mathesar/utils/frontend.py index 428017f7f0..8c0ad8fff4 100644 --- a/mathesar/utils/frontend.py +++ b/mathesar/utils/frontend.py @@ -21,11 +21,14 @@ def get_manifest_data(): module_data = raw_data['src/main.ts'] manifest_data['module_css'] = [filename for filename in module_data['css']] manifest_data['module_js'] = module_data['file'] - legacy_data = raw_data['src/main-legacy.ts'] manifest_data['legacy_polyfill_js'] = raw_data['vite/legacy-polyfills-legacy']['file'] manifest_data['legacy_js'] = legacy_data['file'] + for locale, _ in settings.LANGUAGES or []: + manifest_data[locale] = raw_data[f'src/i18n/{locale}/index.ts'] + manifest_data[f"{locale}-legacy"] = raw_data[f'src/i18n/{locale}/index-legacy.ts'] + # Cache data for 1 hour cache.set('manifest_data', manifest_data, 60 * 60) return manifest_data diff --git a/mathesar_ui/src/App.svelte b/mathesar_ui/src/App.svelte index 4fe08c214e..f512425f81 100644 --- a/mathesar_ui/src/App.svelte +++ b/mathesar_ui/src/App.svelte @@ -3,38 +3,42 @@ import { preloadCommonData } from '@mathesar/utils/preloadData'; import AppContext from './AppContext.svelte'; import RootRoute from './routes/RootRoute.svelte'; - import { loadLocaleAsync } from './i18n/i18n-load'; import { setLocale } from './i18n/i18n-svelte'; - import type { RequestStatus } from './api/utils/requestUtils'; - import { getErrorMessage } from './utils/errors'; import ErrorBox from './components/message-boxes/ErrorBox.svelte'; + import { loadLocaleAsync, loadTranslations } from './i18n/i18n-load'; + let isTranslationsLoaded = false; /** - * Later the translations file will be loaded - * in parallel to the FE's first chunk + * Why translations are being read from window object? + * In order to - + * 1. Load the translations file in parallel to the first FE chunk. + * 2. And then make it available for the entry(App.svelte) + * file to load them into memory. + * + * The index.html loads it as using a script tag + * Each translations file on load, attaches the translations + * to the window object */ - let translationLoadStatus: RequestStatus = { state: 'processing' }; void (async () => { - try { + const { translations, displayLanguage } = window.Mathesar || {}; + if (translations && displayLanguage) { + loadTranslations(displayLanguage, translations[displayLanguage]); + setLocale(displayLanguage); + isTranslationsLoaded = true; + } else { await loadLocaleAsync('en'); - setLocale('en'); - translationLoadStatus = { state: 'success' }; - } catch (exp) { - translationLoadStatus = { - state: 'failure', - errors: [getErrorMessage(exp)], - }; + isTranslationsLoaded = true; } })(); const commonData = preloadCommonData(); -{#if translationLoadStatus.state === 'success' && commonData} +{#if isTranslationsLoaded && commonData} -{:else if translationLoadStatus.state === 'processing'} +{:else if !isTranslationsLoaded}
diff --git a/mathesar_ui/src/global.d.ts b/mathesar_ui/src/global.d.ts index efcd02a534..307c3e0ddc 100644 --- a/mathesar_ui/src/global.d.ts +++ b/mathesar_ui/src/global.d.ts @@ -5,3 +5,12 @@ declare module '*.mdx' { const value: string; export default value; } + +interface Window { + Mathesar: + | { + displayLanguage: Locales; + translations: Record | undefined; + } + | undefined; +} diff --git a/mathesar_ui/src/i18n/en/index.ts b/mathesar_ui/src/i18n/en/index.ts index 5319277860..e9bc18f660 100644 --- a/mathesar_ui/src/i18n/en/index.ts +++ b/mathesar_ui/src/i18n/en/index.ts @@ -1,4 +1,5 @@ -import type { BaseTranslation } from '../i18n-types.js'; +import type { BaseTranslation, Translations } from '../i18n-types'; +import { addTranslationsToGlobalObject } from '../i18n-util'; const en: BaseTranslation = { general: { @@ -32,3 +33,5 @@ const en: BaseTranslation = { }; export default en; + +addTranslationsToGlobalObject('en', en as Translations); diff --git a/mathesar_ui/src/i18n/formatters.ts b/mathesar_ui/src/i18n/formatters.ts index a6d0c90e49..6da42ded3e 100644 --- a/mathesar_ui/src/i18n/formatters.ts +++ b/mathesar_ui/src/i18n/formatters.ts @@ -1,5 +1,5 @@ import type { FormattersInitializer } from 'typesafe-i18n'; -import type { Locales, Formatters } from './i18n-types.js'; +import type { Locales, Formatters } from './i18n-types'; export const initFormatters: FormattersInitializer< Locales, diff --git a/mathesar_ui/src/i18n/i18n-load.ts b/mathesar_ui/src/i18n/i18n-load.ts index 02f828295e..21fef51b23 100644 --- a/mathesar_ui/src/i18n/i18n-load.ts +++ b/mathesar_ui/src/i18n/i18n-load.ts @@ -1,6 +1,6 @@ -import { initFormatters } from './formatters.js'; -import type { Locales, Translations } from './i18n-types.js'; -import { loadedFormatters, loadedLocales } from './i18n-util.js'; +import { initFormatters } from './formatters'; +import type { Locales, Translations } from './i18n-types'; +import { loadedFormatters, loadedLocales } from './i18n-store'; const localeTranslationLoaders = { ja: () => import('./ja/index.js'), @@ -30,3 +30,8 @@ export async function loadLocaleAsync(locale: Locales): Promise { updateTranslationsDictionary(locale, await importLocaleAsync(locale)); loadFormatters(locale); } + +export function loadTranslations(locale: Locales, translations: Translations) { + updateTranslationsDictionary(locale, translations); + loadFormatters(locale); +} diff --git a/mathesar_ui/src/i18n/i18n-store.ts b/mathesar_ui/src/i18n/i18n-store.ts new file mode 100644 index 0000000000..8f1caebb7b --- /dev/null +++ b/mathesar_ui/src/i18n/i18n-store.ts @@ -0,0 +1,11 @@ +import type { Formatters, Locales, Translations } from './i18n-types'; + +export const loadedLocales: Record = {} as Record< + Locales, + Translations +>; + +export const loadedFormatters: Record = {} as Record< + Locales, + Formatters +>; diff --git a/mathesar_ui/src/i18n/i18n-svelte.ts b/mathesar_ui/src/i18n/i18n-svelte.ts index 77e2f482a9..97a670895f 100644 --- a/mathesar_ui/src/i18n/i18n-svelte.ts +++ b/mathesar_ui/src/i18n/i18n-svelte.ts @@ -4,8 +4,8 @@ import type { Locales, TranslationFunctions, Translations, -} from './i18n-types.js'; -import { loadedFormatters, loadedLocales } from './i18n-util.js'; +} from './i18n-types'; +import { loadedFormatters, loadedLocales } from './i18n-store'; const { locale, LL, setLocale } = initI18nSvelte< Locales, diff --git a/mathesar_ui/src/i18n/i18n-util.ts b/mathesar_ui/src/i18n/i18n-util.ts index d0637bfa55..858fb6c9dc 100644 --- a/mathesar_ui/src/i18n/i18n-util.ts +++ b/mathesar_ui/src/i18n/i18n-util.ts @@ -1,14 +1,26 @@ import { initExtendDictionary } from 'typesafe-i18n/utils'; -import type { Formatters, Locales, Translations } from './i18n-types.js'; - -export const loadedLocales: Record = {} as Record< - Locales, - Translations ->; - -export const loadedFormatters: Record = {} as Record< - Locales, - Formatters ->; +import type { Locales, Translations } from './i18n-types'; export const extendDictionary = initExtendDictionary(); + +export function addTranslationsToGlobalObject( + locale: Locales, + translations: Translations, +) { + /** + * This function is being called by all of the translations files + * The base translation file is being loaded by the typesafe-i18n utility + * to generate types during the development time. + * Hence this function also runs in the context of node + * instead of just browser. + */ + if (typeof window === 'undefined') return; + window.Mathesar = { + ...window.Mathesar, + displayLanguage: locale, + translations: { + ...window.Mathesar?.translations, + [locale]: translations, + }, + }; +} diff --git a/mathesar_ui/src/i18n/ja/index.ts b/mathesar_ui/src/i18n/ja/index.ts index 236baf6231..7c99872b30 100644 --- a/mathesar_ui/src/i18n/ja/index.ts +++ b/mathesar_ui/src/i18n/ja/index.ts @@ -1,7 +1,12 @@ -import en from '../en/index.js'; -import type { Translation } from '../i18n-types.js'; -import { extendDictionary } from '../i18n-util.js'; +import en from '../en/index'; +import type { Translation } from '../i18n-types'; +import { + addTranslationsToGlobalObject, + extendDictionary, +} from '../i18n-util.js'; const ja = extendDictionary(en, {}) as Translation; export default ja; + +addTranslationsToGlobalObject('en', ja); diff --git a/mathesar_ui/vite.config.js b/mathesar_ui/vite.config.js index d9763d0f3a..17d3b5884f 100644 --- a/mathesar_ui/vite.config.js +++ b/mathesar_ui/vite.config.js @@ -38,7 +38,11 @@ export default defineConfig({ build: { manifest: true, rollupOptions: { - input: './src/main.ts', + input: { + main: './src/main.ts', + en: './src/i18n/en/index.ts', + ja: './src/i18n/ja/index.ts', + }, }, outDir: '../mathesar/static/mathesar/', emptyOutDir: true,