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

Conversation

rvveber
Copy link
Collaborator

@rvveber rvveber commented Apr 8, 2025

Adds ability to inject/override translations at runtime.
Moves contents of docs/theming.md into docs/customization.md for a more unified documentation.

@rvveber rvveber force-pushed the feature/custom-translations-frontend branch 2 times, most recently from 10cd759 to d285bd1 Compare April 8, 2025 14:36
@rvveber rvveber mentioned this pull request Apr 8, 2025
9 tasks
@rvveber rvveber force-pushed the feature/custom-translations-frontend branch 2 times, most recently from 4ff16fa to 81fd1ed Compare April 8, 2025 15:12
@AntoLC AntoLC requested review from AntoLC and lunika April 8, 2025 15:19
void synchronizeLanguage();
}, [synchronizeLanguage]);
void synchronizeLanguage().finally(() => {
void customizeTranslations();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could it be done in the useLanguageSynchronizer hook, after synchronizeLanguage ?

Copy link
Collaborator Author

@rvveber rvveber Apr 10, 2025

Choose a reason for hiding this comment

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

It would tightly couple useLanguageSynchronizer with useTranslationsCustomizer hook.
What benefit would it bring?

Copy link
Collaborator Author

@rvveber rvveber Apr 14, 2025

Choose a reason for hiding this comment

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

I improved it. No more async or void calls and no dependency chains.
Check it out.


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.

## Custom CSS
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think "Runtime Theming" is better, we want that users understand that it is a way to overwrite the theme of the App.

Suggested change
## Custom CSS
## Runtime Theming 🎨

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hm, but currently it is just custom CSS right?

  • For Theming it would need to include an example of overwriting a css variable that is generated by the theme.
  • All customizations are/should be and usually are expected to be at runtime, from a customizers point of view atleast, so explicitly adding runtime, suggest that translations are not customized at runtime. It is confusing
  • Target user group for documentation is one that knows what css is, it's the requirement to theme, i don't think we need to name it something else to explain what is possible to non-technicals. For that maybe the README would be a better place.
  • We want to avoid build-time customizations entirely, and for now all content of this documentation is at run-time, why make it extra?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We want to offer 2 different ways, to override the theme, 1 runtime, 1 buildtime, so it makes sense to say that this one is the runtime way. It depends the case, runtime does not every time fit the infrastructure you are working with, in our case we need the build time way for the moment concerning the theme.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 7 to 8
1. [Translations](#custom-translations)
2. [CSS/Theme](#custom-css)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess "custom-translation" will be less used than adding your theme at runtime, should be in second position I think.

Suggested change
1. [Translations](#custom-translations)
2. [CSS/Theme](#custom-css)
1. [Runtime Theming](#runtime-theming)
2. [Translations](#custom-translations)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 10 to 23
const safeLocalStorage = {
getItem: (key: string): string | null => {
if (typeof window === 'undefined') {
return null;
}
return localStorage.getItem(key);
},
setItem: (key: string, value: string): void => {
if (typeof window === 'undefined') {
return;
}
localStorage.setItem(key, value);
},
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Quite usefull, could maybe be available for the all Impress app. What about adding this code in /src/utils/storages.ts ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 25 to 26
export const useTranslationsCustomizer = () => {
const { data: conf } = useConfig();
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
export const useTranslationsCustomizer = () => {
const { data: conf } = useConfig();
export const useTranslationsCustomizer = (translationsUrl:string) => {

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decouple all required vars as parameters, for both TranslationCustomizer and LanguageSynchronizer

Comment on lines 32 to 34
{
"en": {
"Docs": "Notes",
"Create New Document": "+"
},
"de": {
"Docs": "Notizen",
"Create New Document": "+"
}
}
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
{
"en": {
"Docs": "Notes",
"Create New Document": "+"
},
"de": {
"Docs": "Notizen",
"Create New Document": "+"
}
}
{
"en": {
"translation": {
"Docs": "Notes",
"Create New Document": "+"
}
},
"de": {
"translation": {
"Docs": "Notizen",
"Create New Document": "+"
}
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines 93 to 98
const customUrl = conf.FRONTEND_CUSTOM_TRANSLATIONS_URL;
const cachedUrl = safeLocalStorage.getItem(CACHE_URL_KEY);

// Fast path: If we have cached translations for the same URL
if (cachedUrl === customUrl) {
const cached = safeLocalStorage.getItem(CACHE_KEY);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I suppose you added this cache system to reduce the flickering, right ?

I don't think you should add this cache system, I think this hook should return a variable isTranslationLoading, and here (https://github.com/suitenumerique/docs/blob/main/src/frontend/apps/impress/src/core/config/ConfigProvider.tsx#L49-L54), you can just add:

  if (!conf || isTranslationLoading) {
    return (
      <Box $height="100vh" $width="100vw" $align="center" $justify="center">
        <Loader />
      </Box>
    );
  }

If conf.FRONTEND_CUSTOM_TRANSLATIONS_URL is not set, you can switch directly isTranslationLoading to false.

If really a "persistant" cache system has a strong purpose, we should leverage react-query, but I have the feeling it is ok for the moment.

WDYT ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I cached it, because we are waiting on two async requests here

  1. fetch conf to receive the FRONTEND_CUSTOM_TRANSLATIONS_URL
  2. fetch FRONTEND_CUSTOM_TRANSLATIONS_URL to receive the json

I think it is irresponsible to not cache it, since it would define a new critical-path/bottleneck for render-blocking loading times.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But i'm also seeing now that i should not need to wait for conf.

  1. If we have translations in cache, we should already apply them.
  2. If the parallelly fetched translations are different we should apply them again.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that is a good point (critical-path/bottleneck for render-blocking loading times). I want to apply it to the config endpoint.

What about this pattern, more react-querish ?

import { useQuery } from '@tanstack/react-query';
import { Resource } from 'i18next';

import { APIError, errorCauses } from '@/api';

const LOCAL_STORAGE_KEY = 'CUSTOM_TRANSLATIONS';

function getCachedTranslation() {
  try {
    const jsonString = localStorage.getItem(LOCAL_STORAGE_KEY);
    return jsonString ? (JSON.parse(jsonString) as Resource) : undefined;
  } catch {
    return undefined;
  }
}

function setCachedTranslation(translations: Resource) {
  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(translations));
}

export interface FetchCustomTranslationeParams {
  url?: string;
}

export const fetchCustomTranslation = async ({
  url,
}: FetchCustomTranslationeParams) => {
  if (!url) {
    return null;
  }

  const response = await fetch(url);
  if (!response.ok) {
    throw new APIError(
      'Failed to get the translation',
      await errorCauses(response),
    );
  }

  return response.json() as Promise<Resource>;
};

export const KEY_TRANSLATION = 'custom-translation';

export function useCustomTranslation(url?: string) {
  const cachedData = getCachedTranslation();
  const response = useQuery<Resource | null, APIError>({
    queryKey: [KEY_TRANSLATION, url || ''],
    queryFn: () => fetchCustomTranslation({ url }),
    initialData: cachedData,
    staleTime: 0,
    refetchOnWindowFocus: false,
    refetchOnReconnect: false,
  });

  if (response.data && response.isSuccess) {
    setCachedTranslation(response.data);
  }

  return response;
}

Then to call it, it would be quite easy:

export const useTranslationsCustomizer = (translationUrl?: string) => {
  const { i18n } = useTranslation();
  const { data: translations } = useCustomTranslation(translationUrl);

  useEffect(() => {
    if (!translations) {
      return;
    }

    Object.entries(translations).forEach(([lng, namespaces]) => {
      Object.entries(namespaces).forEach(([ns, value]) => {
        i18next.addResourceBundle(lng, ns, value, true, true);
      });
    });

    const currentLanguage = i18n.language;
    void i18next.changeLanguage(currentLanguage);

    // Emit added event to make sure components re-render
    i18next.emit('added', currentLanguage, 'translation');
  }, [i18n.language, translations]);
};

Copy link
Collaborator

@AntoLC AntoLC Apr 11, 2025

Choose a reason for hiding this comment

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

I made this PR about the config cache optimization: #867
It is more optimized than the example provided.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Optimized the code to be more compact, it didn't need nested queries or anything.

@rvveber rvveber force-pushed the feature/custom-translations-frontend branch from 81fd1ed to b9b16aa Compare April 14, 2025 17:04
@rvveber rvveber force-pushed the feature/custom-translations-frontend branch from b9b16aa to 06b91a1 Compare April 14, 2025 17:14
@rvveber rvveber requested a review from AntoLC April 14, 2025 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants