Skip to content

Feature/custom translations frontend #857

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ and this project adheres to
## Added

- 🚩 add homepage feature flag #861

- ✨(frontend) add customization for translations #857

## [3.1.0] - 2025-04-07

Expand Down
79 changes: 79 additions & 0 deletions docs/customization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Application Customization 🛠️
This document outlines the various ways you can customize our application to suit your specific needs without modifying the core codebase.
#### Available Customization Options
> 1. [Runtime Theming 🎨](#runtime-theming-🎨)
> 1. [Runtime Internationalization 🌐](#runtime-internationalization-🌐)

<br>

# Runtime Theming 🎨

### How to Use

To customize the application's appearance, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file:

```javascript
FRONTEND_CSS_URL=http://example.com/custom-style.css
```

Once you've set this variable, our application will load your custom CSS file and apply the styles to our frontend application.

### Benefits

- **Easy customization** 🔄: Customize the look and feel of our application without requiring any code changes.
- **Flexibility** 🌈: Use any CSS styles to create a custom theme that meets your needs.
- **Runtime theming** ⏱️: Change the theme of our application at runtime, without requiring a restart or recompilation.

### Example Use Case

Let's say you want to change the background color of our application to a custom color. Create a custom CSS file with the following contents:

```css
body {
background-color: #3498db;
}
```

Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the background color to the custom color you specified.

<br>

# Runtime Internationalization 🌐

### How to Use

To provide custom translations, set the `FRONTEND_CUSTOM_TRANSLATIONS_URL` environment variable to the URL of your custom translations JSON file:

```javascript
FRONTEND_CUSTOM_TRANSLATIONS_URL=http://example.com/custom-translations.json
```

Once you've set this variable, our application will load your custom translations and apply them to the user interface.

### Benefits

- **Language control** 🌐: Customize terminology to match your organization's vocabulary.
- **Context-specific language** 📝: Adapt text for your specific use case or industry.

### Example Use Case

Let's say you want to customize some key phrases in the application. Create a JSON file with your custom translations:

```json
{
"en": {
"translation": {
"Docs": "MyApp",
"Create New Document": "+"
}
},
"de": {
"translation": {
"Docs": "MeineApp",
"Create New Document": "+"
}
}
}
```

Then set the `FRONTEND_CUSTOM_TRANSLATIONS_URL` environment variable to the URL of this JSON file. The application will load these translations and override the default ones where specified.
2 changes: 2 additions & 0 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ These are the environmental variables you can set for the impress-backend contai
| FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT | Cache duration of the json footer | 86400 |
| FRONTEND_URL_JSON_FOOTER | Url with a json to configure the footer | |
| FRONTEND_THEME | frontend theme to use | |
| FRONTEND_CSS_URL | URL to a custom CSS file for theming the application | |
| FRONTEND_CUSTOM_TRANSLATIONS_URL | URL to a JSON file containing custom translations for the application | |
| POSTHOG_KEY | posthog key for analytics | |
| CRISP_WEBSITE_ID | crisp website id for support | |
| DJANGO_CELERY_BROKER_URL | celery broker url | redis://redis:6379/0 |
Expand Down
33 changes: 0 additions & 33 deletions docs/theming.md

This file was deleted.

1 change: 1 addition & 0 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1693,6 +1693,7 @@ def get(self, request):
"ENVIRONMENT",
"FRONTEND_CSS_URL",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED",
"FRONTEND_CUSTOM_TRANSLATIONS_URL",
"FRONTEND_FOOTER_FEATURE_ENABLED",
"FRONTEND_THEME",
"MEDIA_BASE_URL",
Expand Down
2 changes: 2 additions & 0 deletions src/backend/core/tests/test_api_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
CRISP_WEBSITE_ID="123",
FRONTEND_CSS_URL="http://testcss/",
FRONTEND_HOMEPAGE_FEATURE_ENABLED=True,
FRONTEND_CUSTOM_TRANSLATIONS_URL="http://test-custom-translations/",
FRONTEND_FOOTER_FEATURE_ENABLED=True,
FRONTEND_THEME="test-theme",
MEDIA_BASE_URL="http://testserver/",
Expand All @@ -43,6 +44,7 @@ def test_api_config(is_authenticated):
"ENVIRONMENT": "test",
"FRONTEND_CSS_URL": "http://testcss/",
"FRONTEND_HOMEPAGE_FEATURE_ENABLED": True,
"FRONTEND_CUSTOM_TRANSLATIONS_URL": "http://test-custom-translations/",
"FRONTEND_FOOTER_FEATURE_ENABLED": True,
"FRONTEND_THEME": "test-theme",
"LANGUAGES": [
Expand Down
3 changes: 3 additions & 0 deletions src/backend/impress/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,9 @@ class Base(Configuration):
FRONTEND_CSS_URL = values.Value(
None, environ_name="FRONTEND_CSS_URL", environ_prefix=None
)
FRONTEND_CUSTOM_TRANSLATIONS_URL = values.Value(
None, environ_name="FRONTEND_CUSTOM_TRANSLATIONS_URL", environ_prefix=None
)

# Posthog
POSTHOG_KEY = values.DictValue(
Expand Down
1 change: 1 addition & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CONFIG = {
ENVIRONMENT: 'development',
FRONTEND_CSS_URL: null,
FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,
FRONTEND_CUSTOM_TRANSLATIONS_URL: null,
FRONTEND_FOOTER_FEATURE_ENABLED: true,
FRONTEND_THEME: 'default',
MEDIA_BASE_URL: 'http://localhost:8083',
Expand Down
61 changes: 61 additions & 0 deletions src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,67 @@ test.describe('Config', () => {
.first(),
).toBeAttached();
});

test('it checks FRONTEND_CUSTOM_TRANSLATIONS_URL config', async ({
page,
}) => {
// Create mock URL for translations
const mockTranslationsUrl =
'http://dummyhost.example.com/translations/custom.json';

// Mock the config endpoint to include the custom translations URL
await page.route('**/api/v1.0/config/', async (route) => {
const request = route.request();
if (request.method().includes('GET')) {
await route.fulfill({
json: {
...config,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
...config,
...CONFIG,

FRONTEND_CUSTOM_TRANSLATIONS_URL: mockTranslationsUrl,
},
});
} else {
await route.continue();
}
});

// Mock the translations endpoint to return our custom translations
await page.route(mockTranslationsUrl, async (route) => {
await route.fulfill({
json: {
en: {
translation: {
Docs: 'CustomDocsEn',
},
},
fr: {
translation: {
Docs: 'CustomDocsFR',
},
},
},
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
});

// Intercept requests to the translations URL
const translationsPromise = page.waitForRequest((req) => {
return req.url() === mockTranslationsUrl;
});

// Navigate to the page
await page.goto('/');

// Verify that the application attempted to load the translations
const translationsRequest = await translationsPromise;
expect(translationsRequest).toBeTruthy();

// Extra test to prove that the translations were applied
await expect(page.getByText('CustomDocsEn')).toBeAttached();
});
});

test.describe('Config: Not loggued', () => {
Expand Down
24 changes: 21 additions & 3 deletions src/frontend/apps/impress/src/core/config/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { PropsWithChildren, useEffect } from 'react';

import { Box } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { useLanguageSynchronizer } from '@/features/language/';
import { useAuthQuery } from '@/features/auth';
import {
useLanguageSynchronizer,
useTranslationsCustomizer,
} from '@/features/language/';
import { useAnalytics } from '@/libs';
import { CrispProvider, PostHogAnalytic } from '@/services';
import { useSentryStore } from '@/stores/useSentryStore';
Expand All @@ -13,10 +17,12 @@ import { useConfig } from './api/useConfig';

export const ConfigProvider = ({ children }: PropsWithChildren) => {
const { data: conf } = useConfig();
const { data: user } = useAuthQuery();
const { setSentry } = useSentryStore();
const { setTheme } = useCunninghamTheme();
const { AnalyticsProvider } = useAnalytics();
const { synchronizeLanguage } = useLanguageSynchronizer();
const { customizeTranslations } = useTranslationsCustomizer();

useEffect(() => {
if (!conf?.SENTRY_DSN) {
Expand All @@ -35,8 +41,20 @@ export const ConfigProvider = ({ children }: PropsWithChildren) => {
}, [conf?.FRONTEND_THEME, setTheme]);

useEffect(() => {
void synchronizeLanguage();
}, [synchronizeLanguage]);
if (!conf?.LANGUAGES || !user) {
return;
}

synchronizeLanguage(conf.LANGUAGES, user);
}, [conf?.LANGUAGES, user, synchronizeLanguage]);

useEffect(() => {
if (!conf?.FRONTEND_CUSTOM_TRANSLATIONS_URL) {
return;
}

customizeTranslations(conf.FRONTEND_CUSTOM_TRANSLATIONS_URL);
}, [conf?.FRONTEND_CUSTOM_TRANSLATIONS_URL, customizeTranslations]);

useEffect(() => {
if (!conf?.POSTHOG_KEY) {
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/apps/impress/src/core/config/api/useConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { APIError, errorCauses, fetchAPI } from '@/api';
import { Theme } from '@/cunningham/';
import { PostHogConf } from '@/services';

interface ConfigResponse {
export interface ConfigResponse {
AI_FEATURE_ENABLED?: boolean;
COLLABORATION_WS_URL?: string;
CRISP_WEBSITE_ID?: string;
Expand All @@ -14,6 +14,7 @@ interface ConfigResponse {
FRONTEND_THEME?: Theme;
LANGUAGES: [string, string][];
LANGUAGE_CODE: string;
FRONTEND_CUSTOM_TRANSLATIONS_URL?: string;
MEDIA_BASE_URL?: string;
POSTHOG_KEY?: PostHogConf;
SENTRY_DSN?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { css } from 'styled-components';

import { DropdownMenu, Icon, Text } from '@/components/';
import { useConfig } from '@/core';
import { useAuthQuery } from '@/features/auth';

import { useLanguageSynchronizer } from './hooks/useLanguageSynchronizer';
import { getMatchingLocales } from './utils/locale';

export const LanguagePicker = () => {
const { t, i18n } = useTranslation();
const { data: conf } = useConfig();
const { data: user } = useAuthQuery();
const { synchronizeLanguage } = useLanguageSynchronizer();
const language = i18n.languages[0];
Settings.defaultLocale = language;
Expand All @@ -28,15 +30,17 @@ export const LanguagePicker = () => {
i18n
.changeLanguage(backendLocale)
.then(() => {
void synchronizeLanguage('toBackend');
if (conf?.LANGUAGES && user) {
synchronizeLanguage(conf.LANGUAGES, user, 'toBackend');
}
})
.catch((err) => {
console.error('Error changing language', err);
});
};
return { label, isSelected, callback };
});
}, [conf, i18n, language, synchronizeLanguage]);
}, [conf?.LANGUAGES, i18n, language, synchronizeLanguage, user]);

// Extract current language label for display
const currentLanguageLabel =
Expand Down
Loading
Loading