62
loading...
This website collects cookies to deliver better user experience
You have {itemsCount} items in your cart
. In ideal scenario we'd like something like this:<T keyName="cart_content_key" parameters={{itemsCount: 5}}/>
My main working tool is React and so I use React as kind of reference implementation
title
or aria-label
. For these cases we can't use component, but we need some user callable function, which will translate the text for him imperatively. E.g.:<div title={t('translation_key')}>...</div>
tolgee/core
instance and ideally we don't want limit it for just one (user might want to use multiple different sources of translations in the same project). Ideal would be to provide the instance globally or to a subtree of components in the application (In React it's exactly what Context API does).export const TolgeeProviderContext = React.createContext(null);
export const TolgeeProvider = ({config, children}) => {
const [tolgee] = useState(new Tolgee(config));
useEffect(() => {
tolgee.run();
return () => {
tolgee.stop();
};
}, []);
return (
<TolgeeProviderContext.Provider value={{tolgee}}>
{children}
</TolgeeProviderContext.Provider>
);
};
T
component:export const T = ({ keyName, parameters }) =>
{
const {tolgee} = useContext(TolgeeProviderContext);
// get initial value
const [translated, setTranslated] = useState(
tolgee.instant(keyName, parameters)
);
useEffect(() => {
// subscribe to translation changes
const subscription =
tolgee.onTranslationChange.subscribe((data) => {
if (data.key === keyName) {
setTranslate(tolgee.instant(keyName, parameters));
}
});
return () => {
subscription.unsubscribe();
};
}, [keyName, parameters]);
return translated;
};
tolgee.instant()
, we'll get current translation of the key with applied parameters, also we can subscribe to translation changes, which happen e.g. when language is changed or when user manually changes the translation through in-context localization tool.t
function in React, we need to somehow "hook" it to component lifecycle as we need not just to translate keys, but also subscribe the component to translation changes. Simplest solution (as I might given you a hint) is to create hook, which returns this t
function. Then we have separate t
function for each component and we can connect it to the component lifecycle. Let's look how that can look like.export const useTranslate: () => ReturnFnType = () => {
const {tolgee} = useTolgeeContext();
// dummy state to force component to re-render
const [_, setDummyValue] = useState(0);
const reRender = () => {
setDummyValue((v) => v + 1);
};
// array of used keys
const usedKeysRef = useRef(new Set());
useEffect(() => {
const subscription = tolgee.onTranslationChange.subscribe((data) => {
// check if we are using this key
if (usedKeysRef.current.has(data.key)) {
reRender()
}
});
return () => subscription.unsubscribe();
}, []);
return (keyName, parameters) => {
// remember that this key was used
usedKeysRef.current.add(keyName)
return tolgee.instant(keyName, parameters)
};
};
React doesn't have a direct way how to force component to re-render, so we do it through dummy state update.
export const MyComponent = () => {
const t = useTranslate()
return <div title={t('title_key')}>...</div>
}
provide
method on any component.export const TolgeeProvider = {
name: 'TolgeeProvider',
props: {
config: {type: Object, required: true},
},
created() {
const tolgee = new Tolgee({...this.$props.config});
this.tolgeeContext.tolgee = tolgee;
tolgee.run()
},
data() {
return {
tolgeeContext: {
tolgee: null,
// more reactive properties here
},
};
},
provide() {
return {
tolgeeContext: this.tolgeeContext,
};
},
beforeUnmount() {
this.tolgeeContext.tolgee.stop();
},
render() {
this.$slots.default()
},
};
data
property, will get turned into reactive objects (which are watching for changes), so that way Vue triggers re-renedering. In example above, we are putting a whole tolgeeContext
into data property because in reality we need more things to be passed down and it needs to be reactive.T
component can work.export const T = {
name: 'T',
inject: ['tolgeeContext'],
props: {
keyName: {type: String, required: true},
parameters: Object,
},
data() {
const tolgeeContext = this.tolgeeContext;
return {
translation:
tolgeeContext.tolgee.instant(
this.$props.keyName,
this.$props.parameters
)
};
},
methods: {
translate(data) {
if (data.key === this.$props.keyName) {
this.$data.translation =
tolgeeContext.tolgee.instant(
this.$props.keyName,
this.$props.parameters
)
}
}
},
created() {
const tolgeeContext = this.tolgeeContext;
this.$options.subscription =
tolgeeContext.tolgee.onTranslationChange.subscribe(this.translate);
},
beforeUnmount() {
this.$options.subscription.unsubscribe();
},
render() {
return this.$data.translation
},
};
tolgeeContext
through inject
property and subscribe toMixin
api. Mixin is a way how to share logic between components while using it's lifecycle methods.export const TolgeeMixin = {
inject: ['tolgeeContext'],
beforeCreate() {
this.$options.usedKeys = new Set()
},
created() {
const tolgeeContext = this.tolgeeContext;
this.$options.subscription =
tolgeeContext.tolgee.onTranslationChange.subscribe((data) => {
if (this.$options.usedKeys.has(data.key)) {
this.$forceUpdate();
}
});
},
methods: {
$t(keyName, params) {
this.$options.usedKeys.add(keyName)
const tolgeeContext = this.tolgeeContext;
return tolgeeContext.tolgee.instant(keyName, params);
},
},
beforeUnmount() {
this.$options.subscription.unsubscribe();
},
};
<template>
<div :title="$t('title_key')">...</div>
</template>
<script>
export const Component = {
mixins: [TolgeeMixin],
};
</script>
tolgeeContext
, subscribe into translation changes and add $t
method, which can then be used by the user. We also maintain a list of keys, which were used and we update the component when they change. Notice that Vue has explicit method $forceUpdate
, which causes re-render of the component.Svelte and Angular sections are written by Jan Cizmar, as he is the author of the integrations
<!-- TolgeeProvider.svelte -->
<script>
import { onDestroy, onMount, setContext } from "svelte";
export let config: TolgeeConfig;
const tolgee = new Tolgee(config);
setContext("tolgeeContext", { tolgee });
onMount(() => tolgee.run());
onDestroy(() => tolgee.stop());
</script>
<slot />
T
component that uses the context looks like this:<script>
import { onDestroy, getContext } from "svelte";
export let keyName;
export let parameters;
const tolgeeContext = getContext('tolgeeContext');
let translated
const translate = () => {
translated = tolgeeContext.tolgee.instant(
keyName,
parameters,
);
}
translate();
const subscription =
tolgeeContext.tolgee.onTranslationChange.subscribe((data) => {
if (data.key === keyName) {
translate()
}
});
onDestroy(() => {
subscription.unsubscribe();
});
</script>
{translated}
TolgeeProvider
and T
component are very similar to React implementation.getTranslate
method, which in Svelte returns a Store containing the method to actually translate the key.import {onDestroy, getContext} from 'svelte';
import {derived, writable} from 'svelte/store';
export const getTranslate = () => {
const context = getContext('tolgeeContext');
const tolgee = context.tolgee;
// set of used keys
const usedKeys = new Set();
// dummy store which is updated to forces providing of new translate method
const updateStore = writable(0);
const update = () => updateStore.update((number) => number + 1);
const translate = (keyName, params) => {
usedKeys.add(keyName);
return tolgee.instant(keyName, params);
};
const subscription =
tolgee.onTranslationChange.subscribe((data) => {
if (usedKeys.has(data.key)) {
update();
}
});
onDestroy(() => {
subscription.unsubscribe();
});
// return new translate method when something is changed
return derived(
// when updateStore changes, translate function gets changed as well
updateStore,
() => (keyName, params) => {
return translate(keyName, params);
}
);
};
getTranslate
function:<script>
const t = getTranslate();
</script>
<div title={$t('title_key')}>...</div>
useTranslate
is hook, which is basically modifying state to force rerender when translation changes and the returned t
function called by the new render then returns new value.t
is actually a Store containing the translating function. That's why there is the $
before the function call. It means that the value should be re-rendered whenever the Store is updated.@NgModule({
declarations: [TranslatePipe, STranslatePipe, TComponent],
exports: [TranslatePipe, STranslatePipe, TComponent],
providers: [],
})
export class NgxTolgeeModule {
// @dynamic
static forRoot(options: TolgeeConfig): ModuleWithProviders<NgxTolgeeModule> {
options = {filesUrlPrefix: '/assets/i18n/', ...options};
return {
ngModule: NgxTolgeeModule,
providers: [
TranslateService,
TranslationsProvider,
{
provide: APP_INITIALIZER,
useFactory: (provider: TranslationsProvider) => {
return async () => await provider.load(options);
},
deps: [TranslationsProvider, TranslateService],
multi: true,
},
{provide: TolgeeConfig, useValue: options},
],
};
}
}
import {EventEmitter, Injectable, OnDestroy} from '@angular/core';
import {Observable} from 'rxjs';
import {Tolgee, TranslationData} from '@tolgee/core';
import {TolgeeConfig} from './tolgeeConfig';
@Injectable()
export class TranslateService implements OnDestroy {
constructor(private config: TolgeeConfig) {
}
// Logic creating the Instance of Tolgee and lot of other stuff is ommited
// ...
private _tolgee: Tolgee;
public get tolgee(): Tolgee {
return this._tolgee;
}
public translate(
key: string,
params = {},
): Observable<string> {
return new Observable((subscriber) => {
const translate = () => {
const translated = this.tolgee.instant(
key,
params,
);
subscriber.next(translated);
};
translate();
const onTranslationChangeSubscription =
this.tolgee.onTranslationChange.subscribe((data) => {
if (data.key === key) {
translate();
}
});
return () => {
onTranslationChangeSubscription.unsubscribe();
};
});
}
}
translate
method creates new Observable and emits new value every time translation is changed, which is emitted by Tolgee instance from @tolgee/core library. It also returns function, which enables us to unsubscribe.T
component in React, we can use t
selector, it subscribes to translate
Observable and changes the result according to the new value.import {Component, ElementRef, Input, OnDestroy, OnInit} from '@angular/core';
import {Subscription} from 'rxjs';
import {TranslateService} from './translate.service';
import {TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE} from '@tolgee/core';
@Component({
selector: '[t]',
template: ``,
})
export class TComponent implements OnInit, OnDestroy {
@Input() params?: Record<string, any>;
@Input() key: string;
subscription: Subscription;
constructor(
private ref: ElementRef,
private translateService: TranslateService
) {
}
ngOnInit(): void {
const element = this.ref.nativeElement as HTMLElement;
element.setAttribute(TOLGEE_WRAPPED_ONLY_DATA_ATTRIBUTE, this.key);
this.subscription = this.translateService
.translate(this.key, this.params, this.default)
.subscribe((translated) => {
return (element.textContent = translated);
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
import {OnDestroy, Pipe, PipeTransform} from '@angular/core';
import {TranslateService} from './translate.service';
import {Subscription} from 'rxjs';
@Pipe({
name: 'translate',
pure: false,
})
export class TranslatePipe implements PipeTransform, OnDestroy {
value = '';
key: string;
params: Record<string, any>;
private subscription: Subscription;
constructor(protected translateService: TranslateService) {
}
ngOnDestroy(): void {
this.unsubscribe();
}
transform(
key: any,
params?: Record<string, any>
): string {
if (
this.key === key &&
JSON.stringify(this.params) === JSON.stringify(params)
) {
// parameters unchanged
return this.value;
}
this.key = key;
this.params = params;
this.unsubscribe();
this.subscription = this.translate(key, params);
return this.value;
}
private unsubscribe() {
this.subscription.unsubscribe();
}
private translate(key, params) {
this.translateService.translate(key, params).subscribe((r) => {
this.value = r;
});
}
}
PS: Check Tolgee.io and give us github stars