core-app-api: refactor TranslationApi to use AppLanguageApi
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
+1
-1
@@ -21,7 +21,7 @@ import { Observable } from '@backstage/types';
|
||||
import { BehaviorSubject } from '../../../lib';
|
||||
|
||||
const STORAGE_KEY = 'language';
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
export const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
/** @alpha */
|
||||
export interface AppLanguageSelectorOptions {
|
||||
|
||||
+48
-31
@@ -21,6 +21,7 @@ import {
|
||||
TranslationSnapshot,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { AppLanguageSelector } from '../AppLanguageApi';
|
||||
import { I18nextTranslationApi } from './I18nextTranslationApi';
|
||||
|
||||
const plainRef = createTranslationRef({
|
||||
@@ -78,30 +79,34 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should get a translation snapshot', () => {
|
||||
const translationApi = I18nextTranslationApi.create();
|
||||
expect(translationApi.getAvailableLanguages()).toEqual(['en']);
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi: AppLanguageSelector.create(),
|
||||
});
|
||||
|
||||
const snapshot = assertReady(translationApi.getTranslation(plainRef));
|
||||
expect(snapshot.t('foo')).toBe('Foo');
|
||||
});
|
||||
|
||||
it('should get a translation snapshot for ref with translations', async () => {
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv'],
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'sv'],
|
||||
});
|
||||
expect(translationApi.getAvailableLanguages()).toEqual(['en', 'sv']);
|
||||
const translationApi = I18nextTranslationApi.create({ languageApi });
|
||||
|
||||
expect(translationApi.getTranslation(resourceRef).ready).toBe(true);
|
||||
await translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('sv');
|
||||
await 'a tick';
|
||||
expect(translationApi.getTranslation(resourceRef).ready).toBe(false);
|
||||
});
|
||||
|
||||
it('should wait for translations to be loaded', async () => {
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv'],
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'sv'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({ languageApi });
|
||||
expect(translationApi.getTranslation(resourceRef).ready).toBe(true);
|
||||
await translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('sv');
|
||||
await 'a tick';
|
||||
expect(translationApi.getTranslation(resourceRef).ready).toBe(false);
|
||||
|
||||
const snapshot = assertReady(
|
||||
@@ -111,7 +116,9 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should create an instance with message overrides', () => {
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationMessages({
|
||||
ref: plainRef,
|
||||
@@ -124,7 +131,9 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should create an instance and ignore null overrides', () => {
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationMessages({
|
||||
ref: plainRef,
|
||||
@@ -138,8 +147,11 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should create an instance with translation resources', async () => {
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'sv'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv'],
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -150,7 +162,8 @@ describe('I18nextTranslationApi', () => {
|
||||
],
|
||||
});
|
||||
|
||||
await translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('sv');
|
||||
await 'a tick';
|
||||
|
||||
expect(translationApi.getTranslation(plainRef).ready).toBe(false);
|
||||
|
||||
@@ -161,7 +174,9 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should wait for default language translations to be loaded', async () => {
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -179,8 +194,11 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should prefer the last loaded resource', async () => {
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'sv'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv'],
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: resourceRef,
|
||||
@@ -204,7 +222,7 @@ describe('I18nextTranslationApi', () => {
|
||||
],
|
||||
});
|
||||
|
||||
await translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('sv');
|
||||
|
||||
const snapshot = assertReady(
|
||||
await waitForNext(translationApi.translation$(resourceRef), s => s.ready),
|
||||
@@ -213,19 +231,10 @@ describe('I18nextTranslationApi', () => {
|
||||
expect(snapshot.t('bar')).toBe('Bär');
|
||||
});
|
||||
|
||||
it('should refuse switch on unsupported languages', async () => {
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv'],
|
||||
});
|
||||
expect(translationApi.getAvailableLanguages()).toEqual(['en', 'sv']);
|
||||
await translationApi.changeLanguage('sv');
|
||||
await expect(translationApi.changeLanguage('de')).rejects.toThrow(
|
||||
"Failed to change language to 'de', available languages are 'en', 'sv",
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward loading errors', async () => {
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -240,10 +249,12 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should only call the loader once', async () => {
|
||||
const languageApi = AppLanguageSelector.create();
|
||||
const loader = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ default: { foo: 'OtherFoo' } });
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -268,8 +279,11 @@ describe('I18nextTranslationApi', () => {
|
||||
});
|
||||
|
||||
it('should handle interrupted loads gracefully', async () => {
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'sv', 'no'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'sv', 'no'],
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -288,17 +302,20 @@ describe('I18nextTranslationApi', () => {
|
||||
);
|
||||
expect(enSnapshot.t('foo')).toBe('foo');
|
||||
|
||||
translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('sv');
|
||||
const nextPromise = waitForNext(translationApi.translation$(plainRef));
|
||||
translationApi.changeLanguage('no');
|
||||
languageApi.setLanguage('no');
|
||||
|
||||
const snapshot = assertReady(await nextPromise);
|
||||
expect(snapshot.t('foo')).toBe('Føø');
|
||||
});
|
||||
|
||||
it('should only emit changes', async () => {
|
||||
const languageApi = AppLanguageSelector.create({
|
||||
availableLanguages: ['en', 'dk', 'sv', 'no'],
|
||||
});
|
||||
const translationApi = I18nextTranslationApi.create({
|
||||
supportedLanguages: ['en', 'dk', 'sv', 'no'],
|
||||
languageApi,
|
||||
resources: [
|
||||
createTranslationResource({
|
||||
ref: plainRef,
|
||||
@@ -320,10 +337,10 @@ describe('I18nextTranslationApi', () => {
|
||||
translations.push(translation);
|
||||
|
||||
if (translation === 'foo') {
|
||||
translationApi.changeLanguage('dk'); // Not visible
|
||||
translationApi.changeLanguage('sv');
|
||||
languageApi.setLanguage('dk'); // Not visible
|
||||
languageApi.setLanguage('sv');
|
||||
} else if (translation === 'Föö') {
|
||||
translationApi.changeLanguage('no');
|
||||
languageApi.setLanguage('no');
|
||||
} else if (translation === 'Føø') {
|
||||
resolve();
|
||||
subscription.unsubscribe();
|
||||
|
||||
+28
-32
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AppLanguageApi,
|
||||
TranslationApi,
|
||||
TranslationMessages,
|
||||
TranslationRef,
|
||||
@@ -36,14 +37,13 @@ import {
|
||||
InternalTranslationRef,
|
||||
} from '../../../../../core-plugin-api/src/translation/TranslationRef';
|
||||
import { Observable } from '@backstage/types';
|
||||
|
||||
const DEFAULT_LANGUAGE = 'en';
|
||||
import { DEFAULT_LANGUAGE } from '../AppLanguageApi/AppLanguageSelector';
|
||||
|
||||
/** @alpha */
|
||||
export type ExperimentalI18n = {
|
||||
supportedLanguages?: string[];
|
||||
export interface I18nextTranslationApiOptions {
|
||||
languageApi: AppLanguageApi;
|
||||
resources?: Array<TranslationMessages | TranslationResource>;
|
||||
};
|
||||
}
|
||||
|
||||
function removeNulls(
|
||||
messages: Record<string, string | null>,
|
||||
@@ -139,11 +139,9 @@ class ResourceLoader {
|
||||
|
||||
/** @alpha */
|
||||
export class I18nextTranslationApi implements TranslationApi {
|
||||
static create(options?: ExperimentalI18n) {
|
||||
const languages = options?.supportedLanguages || [DEFAULT_LANGUAGE];
|
||||
if (!languages.includes(DEFAULT_LANGUAGE)) {
|
||||
throw new Error(`Supported languages must include '${DEFAULT_LANGUAGE}'`);
|
||||
}
|
||||
static create(options: I18nextTranslationApiOptions) {
|
||||
const { languages } = options.languageApi.getAvailableLanguages();
|
||||
|
||||
const i18n = createI18n({
|
||||
fallbackLng: DEFAULT_LANGUAGE,
|
||||
supportedLngs: languages,
|
||||
@@ -185,36 +183,27 @@ export class I18nextTranslationApi implements TranslationApi {
|
||||
}
|
||||
}
|
||||
|
||||
return new I18nextTranslationApi(i18n, loader, languages);
|
||||
const instance = new I18nextTranslationApi(
|
||||
i18n,
|
||||
loader,
|
||||
options.languageApi.getLanguage().language,
|
||||
);
|
||||
|
||||
options.languageApi.language$().subscribe(({ language }) => {
|
||||
instance.#changeLanguage(language);
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
#i18n: I18n;
|
||||
#loader: ResourceLoader;
|
||||
#language: string;
|
||||
#languages: string[];
|
||||
|
||||
private constructor(i18n: I18n, loader: ResourceLoader, languages: string[]) {
|
||||
private constructor(i18n: I18n, loader: ResourceLoader, language: string) {
|
||||
this.#i18n = i18n;
|
||||
this.#loader = loader;
|
||||
this.#language = DEFAULT_LANGUAGE;
|
||||
this.#languages = languages;
|
||||
}
|
||||
|
||||
getAvailableLanguages(): string[] {
|
||||
return this.#languages.slice();
|
||||
}
|
||||
|
||||
async changeLanguage(language?: string): Promise<void> {
|
||||
const lng = language ?? DEFAULT_LANGUAGE;
|
||||
if (lng && !this.#languages.includes(lng)) {
|
||||
throw new Error(
|
||||
`Failed to change language to '${lng}', available languages are '${this.#languages.join(
|
||||
"', '",
|
||||
)}'`,
|
||||
);
|
||||
}
|
||||
this.#language = lng;
|
||||
await this.#i18n.changeLanguage(lng);
|
||||
this.#language = language;
|
||||
}
|
||||
|
||||
getTranslation<TMessages extends { [key in string]: string }>(
|
||||
@@ -290,6 +279,13 @@ export class I18nextTranslationApi implements TranslationApi {
|
||||
});
|
||||
}
|
||||
|
||||
#changeLanguage(language: string): void {
|
||||
if (this.#language !== language) {
|
||||
this.#language = language;
|
||||
this.#i18n.changeLanguage(language);
|
||||
}
|
||||
}
|
||||
|
||||
#createSnapshot<TMessages extends { [key in string]: string }>(
|
||||
internalRef: InternalTranslationRef<string, TMessages>,
|
||||
): TranslationSnapshot<TMessages> {
|
||||
|
||||
Reference in New Issue
Block a user