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 10 commits into
base: main
Choose a base branch
from
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

## Added

- ✨(frontend) add customization for translations #857

## [3.2.1] - 2025-05-06

## Fixed
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_URL_JSON_CUSTOM_TRANSLATIONS` environment variable to the URL of your custom translations JSON file:

```javascript
FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS=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",
"New doc": "+"
}
},
"de": {
"translation": {
"Docs": "MeineApp",
"New doc": "+"
}
}
}
```

Then set the `FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS` environment variable to the URL of this JSON file. The application will load these translations and override the default ones where specified.
6 changes: 4 additions & 2 deletions docs/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Here we describe all environment variables that can be set for the docs applicat
These are the environmental variables you can set for the impress-backend container.

| Option | Description | default |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
|-------------------------------------------------|-----------------------------------------------------------------------------------------------|---------------------------------------------------------|
| DJANGO_ALLOWED_HOSTS | allowed hosts | [] |
| DJANGO_SECRET_KEY | secret key | |
| DJANGO_SERVER_TO_SERVER_API_TOKENS | | [] |
Expand Down Expand Up @@ -51,7 +51,9 @@ These are the environmental variables you can set for the impress-backend contai
| FRONTEND_HOMEPAGE_FEATURE_ENABLED | frontend feature flag to display the homepage | false |
| FRONTEND_FOOTER_FEATURE_ENABLED | frontend feature flag to display the footer | false |
| 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_CUSTOM_TRANSLATIONS_VIEW_CACHE_TIMEOUT | Cache duration of the json custom translations | 86400 |
| FRONTEND_URL_JSON_FOOTER | Url to a JSON to configure the footer | |
| FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS | Url to a JSON to overwrite the translations | |
| FRONTEND_THEME | frontend theme to use | |
| POSTHOG_KEY | posthog key for analytics | |
| CRISP_WEBSITE_ID | crisp website id for support | |
Expand Down
33 changes: 0 additions & 33 deletions docs/theming.md

This file was deleted.

3 changes: 2 additions & 1 deletion env.d/development/common.dist
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,5 @@ COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/
# Frontend
FRONTEND_THEME=default
FRONTEND_FOOTER_FEATURE_ENABLED=True
FRONTEND_URL_JSON_FOOTER=http://frontend:3000/contents/footer-demo.json
FRONTEND_URL_JSON_FOOTER=http://0.0.0.0:8000/static/contents/footer-demo.json
FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS=http://0.0.0.0:8000/static/contents/custom-translations-demo.json
35 changes: 31 additions & 4 deletions src/backend/core/api/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from django.utils.decorators import method_decorator
from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import cache_page
from django.views.decorators.cache import cache_page, never_cache

import requests
import rest_framework as drf
Expand All @@ -32,7 +32,7 @@
from core import authentication, enums, models
from core.services.ai_services import AIService
from core.services.collaboration_services import CollaborationService
from core.services.config_services import get_footer_json
from core.services.config_services import debug_enabled, get_json_from_url
from core.utils import extract_attachments, filter_descendants

from . import permissions, serializers, utils
Expand Down Expand Up @@ -1736,15 +1736,42 @@ class FooterView(drf.views.APIView):

permission_classes = [AllowAny]

@method_decorator(cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT))
@method_decorator(
never_cache
if debug_enabled("no-cache")
else cache_page(settings.FRONTEND_FOOTER_VIEW_CACHE_TIMEOUT)
)
def get(self, request):
"""
GET /api/v1.0/footer/
Return the footer JSON.
"""
json_footer = (
get_footer_json(settings.FRONTEND_URL_JSON_FOOTER)
get_json_from_url(settings.FRONTEND_URL_JSON_FOOTER)
if settings.FRONTEND_URL_JSON_FOOTER
else {}
)
return drf.response.Response(json_footer)


class CustomTranslationsView(drf.views.APIView):
"""API ViewSet for sharing the custom-translations JSON."""

permission_classes = [AllowAny]

@method_decorator(
never_cache
if debug_enabled("no-cache")
else cache_page(settings.FRONTEND_CUSTOM_TRANSLATIONS_VIEW_CACHE_TIMEOUT)
)
def get(self, request):
"""
GET /api/v1.0/custom-translations/
Return the custom-translations JSON.
"""
json = (
get_json_from_url(settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS)
if settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS
else {}
)
return drf.response.Response(json)
28 changes: 21 additions & 7 deletions src/backend/core/services/config_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,38 @@

import logging

from django.conf import settings

import requests

logger = logging.getLogger(__name__)


def get_footer_json(footer_json_url: str) -> dict:
def debug_enabled(namespace: str | None) -> bool:
"""
Quick way to check if debug is enabled for a specific namespace
If no namespace is passed it just returns the value of settings.DEBUG
"""
if not namespace:
return getattr(settings, "DEBUG", False)

if False is getattr(settings, "DEBUG", False):
return False

return namespace in getattr(settings, "DEBUG_NAMESPACES", [])


def get_json_from_url(json_url: str) -> dict:
"""
Fetches the footer JSON from the given URL."
Fetches JSON from the given URL."
"""
try:
response = requests.get(
footer_json_url, timeout=5, headers={"User-Agent": "Docs-Application"}
json_url, timeout=5, headers={"User-Agent": "Docs-Application"}
)
response.raise_for_status()

footer_json = response.json()

return footer_json
return response.json()
except (requests.RequestException, ValueError) as e:
logger.error("Failed to fetch footer JSON: %s", e)
logger.error("Failed to fetch JSON: %s", e)
return {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"en": {
"translation": {
"Docs": "MyDocs",
"New doc": "+"
}
}
}
83 changes: 83 additions & 0 deletions src/backend/core/tests/test_api_custom_translations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Test the custom-translations API."""

import responses
from rest_framework.test import APIClient


def test_api_custom_translations_without_settings_configured(settings):
"""Test the custom-translations API without settings configured."""
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS = None
client = APIClient()
response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {}


@responses.activate
def test_api_custom_translations_with_invalid_request(settings):
"""Test the custom-translations API with an invalid request."""
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS = "https://invalid-request.com"

custom_translations_response = responses.get(
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS, status=404
)

client = APIClient()
response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {}
assert custom_translations_response.call_count == 1


@responses.activate
def test_api_custom_translations_with_invalid_json(settings):
"""Test the custom-translations API with an invalid JSON response."""
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS = "https://valid-request.com"

custom_translations_response = responses.get(
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS, status=200, body="invalid json"
)

client = APIClient()
response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {}
assert custom_translations_response.call_count == 1


@responses.activate
def test_api_custom_translations_with_valid_json(settings):
"""Test the custom-translations API with an invalid JSON response."""
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS = "https://valid-request.com"

custom_translations_response = responses.get(
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS, status=200, json={"foo": "bar"}
)

client = APIClient()
response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {"foo": "bar"}
assert custom_translations_response.call_count == 1


@responses.activate
def test_api_custom_translations_with_valid_json_and_cache(settings):
"""Test the custom-translations API with an invalid JSON response."""
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS = "https://valid-request.com"

custom_translations_response = responses.get(
settings.FRONTEND_URL_JSON_CUSTOM_TRANSLATIONS, status=200, json={"foo": "bar"}
)

client = APIClient()
response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {"foo": "bar"}
assert custom_translations_response.call_count == 1

response = client.get("/api/v1.0/custom-translations/")
assert response.status_code == 200
assert response.json() == {"foo": "bar"}
# The cache should have been used
assert custom_translations_response.call_count == 1
4 changes: 4 additions & 0 deletions src/backend/core/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@
),
path(f"api/{settings.API_VERSION}/config/", viewsets.ConfigView.as_view()),
path(f"api/{settings.API_VERSION}/footer/", viewsets.FooterView.as_view()),
path(
f"api/{settings.API_VERSION}/custom-translations/",
viewsets.CustomTranslationsView.as_view(),
),
]
Loading
Loading