-
Notifications
You must be signed in to change notification settings - Fork 271
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
base: main
Are you sure you want to change the base?
Feature/custom translations frontend #857
Conversation
10cd759
to
d285bd1
Compare
4ff16fa
to
81fd1ed
Compare
void synchronizeLanguage(); | ||
}, [synchronizeLanguage]); | ||
void synchronizeLanguage().finally(() => { | ||
void customizeTranslations(); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
✅
docs/customization.md
Outdated
|
||
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 |
There was a problem hiding this comment.
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.
## Custom CSS | |
## Runtime Theming 🎨 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
docs/customization.md
Outdated
1. [Translations](#custom-translations) | ||
2. [CSS/Theme](#custom-css) | ||
|
There was a problem hiding this comment.
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.
1. [Translations](#custom-translations) | |
2. [CSS/Theme](#custom-css) | |
1. [Runtime Theming](#runtime-theming) | |
2. [Translations](#custom-translations) | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
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); | ||
}, | ||
}; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
export const useTranslationsCustomizer = () => { | ||
const { data: conf } = useConfig(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export const useTranslationsCustomizer = () => { | |
const { data: conf } = useConfig(); | |
export const useTranslationsCustomizer = (translationsUrl:string) => { |
There was a problem hiding this comment.
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
✅
docs/customization.md
Outdated
{ | ||
"en": { | ||
"Docs": "Notes", | ||
"Create New Document": "+" | ||
}, | ||
"de": { | ||
"Docs": "Notizen", | ||
"Create New Document": "+" | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ | |
"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": "+" | |
} | |
} | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
✅
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); |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
- fetch conf to receive the FRONTEND_CUSTOM_TRANSLATIONS_URL
- 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.
There was a problem hiding this comment.
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.
- If we have translations in cache, we should already apply them.
- If the parallelly fetched translations are different we should apply them again.
There was a problem hiding this comment.
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]);
};
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
✅
81fd1ed
to
b9b16aa
Compare
Part of customization PoC
Part of customization PoC
Part of customization PoC
Part of customization PoC
b9b16aa
to
06b91a1
Compare
Adds ability to inject/override translations at runtime.
Moves contents of
docs/theming.md
intodocs/customization.md
for a more unified documentation.