Skip to content

Commit

Permalink
feat: improve fallback and empty namespace treatment (#3243)
Browse files Browse the repository at this point in the history
  • Loading branch information
stepan662 authored Sep 11, 2023
1 parent 58479f8 commit c8f31cb
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 28 deletions.
9 changes: 6 additions & 3 deletions packages/core/src/Controller/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export function Controller({ options }: StateServiceProps) {
state.getLanguage,
state.getInitialOptions,
state.getAvailableLanguages,
getDefaultAndFallbackNs,
getTranslationNs,
getTranslation,
changeTranslation,
Expand Down Expand Up @@ -87,7 +88,7 @@ export function Controller({ options }: StateServiceProps) {
// takes (ns|default, initial ns, fallback ns, active ns)
function getRequiredNamespaces(ns: NsFallback) {
return [
...getFallbackArray(ns || getDefaultNs()),
...getFallbackArray(ns ?? getDefaultNs()),
...state.getRequiredNamespaces(),
];
}
Expand Down Expand Up @@ -142,12 +143,12 @@ export function Controller({ options }: StateServiceProps) {

function getTranslationNs({ key, ns }: KeyAndNamespacesInternal) {
const languages = state.getFallbackLangs();
const namespaces = getDefaultAndFallbackNs(ns || undefined);
const namespaces = getDefaultAndFallbackNs(ns ?? undefined);
return cache.getTranslationNs(namespaces, languages, key);
}

function getTranslation({ key, ns, language }: KeyAndNamespacesInternal) {
const namespaces = getDefaultAndFallbackNs(ns || undefined);
const namespaces = getDefaultAndFallbackNs(ns ?? undefined);
const languages = state.getFallbackLangs(language);
return cache.getTranslationFallback(namespaces, languages, key);
}
Expand Down Expand Up @@ -209,6 +210,8 @@ export function Controller({ options }: StateServiceProps) {
init: init,
getTranslation: getTranslation,
changeTranslation: changeTranslation,
getTranslationNs: getTranslationNs,
getDefaultAndFallbackNs: getDefaultAndFallbackNs,
async changeLanguage(language: string) {
if (
state.getPendingLanguage() === language &&
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/Controller/Plugins/Plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function Plugins(
getLanguage: () => string | undefined,
getInitialOptions: () => TolgeeOptionsInternal,
getAvailableLanguages: () => string[] | undefined,
getFallbackNamespaces: (ns: string | undefined) => string[],
getTranslationNs: (props: KeyAndNamespacesInternal) => string[],
getTranslation: (props: KeyAndNamespacesInternal) => string | undefined,
changeTranslation: ChangeTranslationInterface,
Expand All @@ -56,7 +57,8 @@ export function Plugins(
return {
key,
defaultValue,
ns: getTranslationNs({ key, ns }),
fallbackNamespaces: getFallbackNamespaces(ns),
namespace: getTranslationNs({ key, ns })[0],
translation: getTranslation({
key,
ns,
Expand Down
113 changes: 113 additions & 0 deletions packages/core/src/__test/namespaces.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { Controller } from '../Controller/Controller';

describe('namespace internal function in controller', () => {
it('it works with no default ns defined', () => {
const controller = Controller({
options: {
language: 'en',
staticData: {
en: { test: 'emptyNs' },
'en:common': { test: 'commonNs' },
},
},
});
expect(controller.getRequiredNamespaces()).toEqual(['']);
expect(controller.getTranslationNs({ key: 'test' })).toEqual(['']);
expect(controller.t({ key: 'test' })).toEqual('emptyNs');
});

it('default ns can be overriten with empty string', () => {
const controller = Controller({
options: {
language: 'en',
staticData: {
en: { test: 'emptyNs' },
'en:common': { test: 'commonNs' },
},
defaultNs: 'common',
},
});
expect(controller.getRequiredNamespaces()).toEqual(['common']);
expect(controller.getTranslationNs({ key: 'test' })).toEqual(['common']);
expect(controller.t({ key: 'test' })).toEqual('commonNs');

expect(controller.getTranslationNs({ key: 'test', ns: '' })).toEqual(['']);
expect(controller.t({ key: 'test', ns: '' })).toEqual('emptyNs');
});

it("can be overriten when key doesn't exist", () => {
const controller = Controller({
options: {
language: 'en',
staticData: {
en: { test: 'emptyNs' },
'en:common': { test: 'commonNs' },
},
defaultNs: 'common',
},
});
expect(controller.getRequiredNamespaces()).toEqual(['common']);
expect(controller.getTranslationNs({ key: 'unknown' })).toEqual(['common']);
expect(controller.t({ key: 'unknown' })).toEqual('unknown');

expect(controller.getTranslationNs({ key: 'unknown', ns: '' })).toEqual([
'',
]);
expect(controller.t({ key: 'unknown', ns: '' })).toEqual('unknown');
});

it('returns correct namespaces if there are fallbacks', () => {
const controller = Controller({
options: {
language: 'en',
staticData: {
en: { test: 'emptyNs' },
'en:common': { test: 'commonNs' },
'en:fallback': { test: 'fallbackNs' },
},
defaultNs: 'common',
fallbackNs: ['', 'fallback'],
},
});
expect(controller.getRequiredNamespaces()).toEqual([
'common',
'',
'fallback',
]);
expect(controller.getTranslationNs({ key: 'unknown' })).toEqual([
'common',
'',
'fallback',
]);
expect(controller.t({ key: 'unknown' })).toEqual('unknown');

expect(
controller.getTranslationNs({ key: 'unknown', ns: 'fallback' })
).toEqual(['fallback', '']);
expect(controller.t({ key: 'unknown', ns: '' })).toEqual('unknown');
});

it('returns correct namespaces if there are empty translations', () => {
const controller = Controller({
options: {
language: 'en',
staticData: {
en: { test: 'hello' },
'en:translation': { test: null },
},
ns: ['', 'translation'],
defaultNs: 'translation',
fallbackNs: '',
},
});
expect(controller.getDefaultAndFallbackNs()).toEqual(['translation', '']);
expect(controller.getTranslationNs({ key: 'test' })).toEqual(['']);
expect(controller.t({ key: 'test' })).toEqual('hello');

expect(controller.getTranslationNs({ key: 'unknown' })).toEqual([
'translation',
'',
]);
expect(controller.t({ key: 'unknown' })).toEqual('unknown');
});
});
12 changes: 12 additions & 0 deletions packages/core/src/__test/namespaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,16 @@ describe('language changes', () => {
// test ns is not loaded for spanish
expect(tolgee.t({ key: 'test', ns: 'test' })).toEqual('test');
});

it('default ns can be overriten with empty string', () => {
const tolgee = TolgeeCore().init({
language: 'en',
staticData: {
en: { test: 'emptyNs' },
'en:common': { test: 'commonNs' },
},
defaultNs: 'emptyNs',
});
expect(tolgee.t({ key: 'test', ns: '' })).toEqual('emptyNs');
});
});
3 changes: 2 additions & 1 deletion packages/core/src/types/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ export type UiProps = {
export type UiKeyOption = {
key: string;
defaultValue?: string;
ns: string[];
fallbackNamespaces: string[];
namespace: string;
translation: string | undefined;
};

Expand Down
15 changes: 10 additions & 5 deletions packages/web/src/ui/KeyDialog/KeyDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ type State = {
key: null | string;
defaultValue: undefined | string;
dialogOpened: boolean;
ns: string[];
fallbackNamespaces: string[];
namespace: string;
};

export class KeyDialog extends React.Component<Props, State> {
state = {
key: null,
defaultValue: undefined,
dialogOpened: false,
ns: [],
fallbackNamespaces: [],
namespace: '',
};

constructor(props: Props) {
Expand All @@ -31,14 +33,16 @@ export class KeyDialog extends React.Component<Props, State> {
public translationEdit(
key: string,
defaultValue: string | undefined,
ns: string[]
fallbackNamespaces: string[],
namespace: string
) {
this.setState({
...this.state,
dialogOpened: true,
defaultValue: defaultValue,
key,
ns,
fallbackNamespaces,
namespace,
});
}

Expand All @@ -55,7 +59,8 @@ export class KeyDialog extends React.Component<Props, State> {
defaultValue={this.state.defaultValue || ''}
open={this.state.dialogOpened}
keyName={this.state.key!}
ns={this.state.ns || []}
fallbackNamespaces={this.state.fallbackNamespaces}
namespace={this.state.namespace}
onClose={this.onClose}
>
<TranslationDialog />
Expand Down
8 changes: 6 additions & 2 deletions packages/web/src/ui/KeyDialog/KeyForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export const KeyForm = () => {
const saving = useDialogContext((c) => c.saving);
const success = useDialogContext((c) => c.success);
const keyExists = useDialogContext((c) => c.keyExists);
const ns = useDialogContext((c) => c.ns);
const fallbackNamespaces = useDialogContext((c) => c.fallbackNamespaces);
const selectedNs = useDialogContext((c) => c.selectedNs);
const isAuthorizedTo = usePermissions();

Expand Down Expand Up @@ -152,7 +152,11 @@ export const KeyForm = () => {
</ScKeyHint>
</ScKey>

<NsSelect options={ns} value={selectedNs} onChange={setSelectedNs} />
<NsSelect
options={fallbackNamespaces}
value={selectedNs}
onChange={setSelectedNs}
/>

{ready && (
<ScTagsWrapper>
Expand Down
18 changes: 5 additions & 13 deletions packages/web/src/ui/KeyDialog/dialogContext/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type DialogProps = {
open: boolean;
onClose: () => void;
uiProps: UiProps;
ns: string[];
fallbackNamespaces: string[];
namespace: string;
children: React.ReactNode;
};

Expand All @@ -46,7 +47,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
const [translationsFormTouched, setTranslationsFormTouched] =
useState(false);

const [selectedNs, setSelectedNs] = useState<string>(props.ns[0]);
const [selectedNs, setSelectedNs] = useState<string>(props.namespace);
const [tags, setTags] = useState<string[]>([]);
const isPat = getApiKeyType(props.uiProps.apiKey) === 'tgpat';

Expand Down Expand Up @@ -105,7 +106,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
method: 'get',
query: {
filterKeyName: [props.keyName],
filterNamespace: props.ns,
filterNamespace: [selectedNs],
languages: selectedLanguages,
},
options: {
Expand All @@ -131,15 +132,6 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
},
});

const namespaces = useMemo(() => {
const keys = translationsLoadable.data?._embedded?.keys;
if (keys?.length) {
return [keys[0].keyNamespace || ''];
} else {
return props.ns;
}
}, [translationsLoadable.data]);

const updateKey = useApiMutation({
url: '/v2/projects/keys/{id}/complex-update',
method: 'put',
Expand Down Expand Up @@ -404,7 +396,7 @@ export const [DialogProvider, useDialogActions, useDialogContext] =
const contextValue = {
input: props.keyName,
open: props.open,
ns: namespaces,
fallbackNamespaces: props.fallbackNamespaces,
selectedNs,
loading,
saving,
Expand Down
17 changes: 14 additions & 3 deletions packages/web/src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,16 @@ export class UI implements UiInterface {
public renderViewer(
key: string,
defaultValue: string | undefined,
ns: string[]
fallbackNamespaces: string[],
namespace: string
) {
this.checkInitialization();
this.viewerComponent?.translationEdit(key, defaultValue, ns);
this.viewerComponent?.translationEdit(
key,
defaultValue,
fallbackNamespaces,
namespace
);
}

public async getKey(props: {
Expand Down Expand Up @@ -86,7 +92,12 @@ export class UI implements UiInterface {
}
if (key) {
const value = keysAndDefaults.find((val) => val.key === key)!;
this?.renderViewer(key, value.defaultValue, value.ns);
this?.renderViewer(
key,
value.defaultValue,
value.fallbackNamespaces,
value.namespace
);
}
}
}

0 comments on commit c8f31cb

Please sign in to comment.