Skip to content

Commit

Permalink
feat: added string util for easy translations
Browse files Browse the repository at this point in the history
Signed-off-by: Sahil <sahil@harness.io>
  • Loading branch information
SahilKr24 committed Jun 29, 2023
1 parent eec8443 commit f886169
Show file tree
Hide file tree
Showing 7 changed files with 1,056 additions and 0 deletions.
63 changes: 63 additions & 0 deletions chaoscenter/web/src/strings/String.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { get } from 'lodash-es';
import { render } from 'mustache';
import { useStringsContext, StringKeys } from './StringsContext';

export interface UseStringsReturn {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getString(key: StringKeys, vars?: Record<string, any>): string;
}

export function useStrings(): UseStringsReturn {
const { data: strings, getString } = useStringsContext();

return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getString(key: StringKeys, vars: Record<string, any> = {}) {
if (typeof getString === 'function') {
return getString(key, vars);
}

const template = get(strings, key);

if (typeof template !== 'string') {
throw new Error(`No valid template with id "${key}" found in any namespace`);
}

return render(template, { ...vars, $: strings });
}
};
}

export interface StringProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
stringID: StringKeys;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vars?: Record<string, any>;
useRichText?: boolean;
tagName: keyof JSX.IntrinsicElements;
}

export function String(props: StringProps): React.ReactElement | null {
const { stringID, vars, useRichText, tagName: Tag, ...rest } = props;
const { getString } = useStrings();

try {
const text = getString(stringID, vars);

return useRichText ? (
<Tag {...(rest as unknown)} dangerouslySetInnerHTML={{ __html: text }} />
) : (
<Tag {...(rest as unknown)}>{text}</Tag>
);
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
return <Tag style={{ color: 'var(--red-500)' }}>{(e as any).message}</Tag>;
}

return null;
}
}

String.defaultProps = {
tagName: 'span'
};
18 changes: 18 additions & 0 deletions chaoscenter/web/src/strings/StringsContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

import type { StringsMap } from './types';

export type StringKeys = keyof StringsMap;

export type { StringsMap };

export interface StringsContextValue {
data: StringsMap;
getString?(key: StringKeys, vars?: Record<string, any>): string;
}

export const StringsContext = React.createContext<StringsContextValue>({} as StringsContextValue);

export function useStringsContext(): StringsContextValue {
return React.useContext(StringsContext);
}
24 changes: 24 additions & 0 deletions chaoscenter/web/src/strings/StringsContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';

import { StringsContext, StringsContextValue, StringsMap } from './StringsContext';

export interface StringsContextProviderProps extends Pick<StringsContextValue, 'getString'> {
children: React.ReactNode;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initialStrings?: Record<string, any>; // temp prop for backward compatability
}

export function StringsContextProvider(props: StringsContextProviderProps): React.ReactElement {
return (
<StringsContext.Provider
value={{
data: {
...(props.initialStrings as StringsMap)
},
getString: props.getString
}}
>
{props.children}
</StringsContext.Provider>
);
}
152 changes: 152 additions & 0 deletions chaoscenter/web/src/strings/__tests__/Strings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';

import { String, useStrings } from '../String';
import { StringsContext } from '../StringsContext';

const value = {
data: {
a: { b: 'Test Value 1' },
chaos: 'Chaos',
test: '{{ $.a.b }} in template',
test2: '{{ $.test }} again'
}
};
describe('String tests', () => {
test('renders strings with simple id', () => {
const { container } = render(
<StringsContext.Provider value={value as any}>
<String stringID={'chaos' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
Chaos
</span>
</div>
`);
});

test('renders error when key not found', () => {
const { container } = render(
<StringsContext.Provider value={value as any}>
<String stringID={'chaos' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
Chaos
</span>
</div>
`);
});

test('renders strings with nested value', () => {
const { container } = render(
<StringsContext.Provider value={value as any}>
<String stringID={'a.b' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
Test Value 1
</span>
</div>
`);
});

test('renders strings with self reference values', () => {
const { container } = render(
<StringsContext.Provider value={value as any}>
<String stringID={'test' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
Test Value 1 in template
</span>
</div>
`);
});

test('self reference only works for one level', () => {
const { container } = render(
<StringsContext.Provider value={value as any}>
<String stringID={'test2' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
{{ $.a.b }} in template again
</span>
</div>
`);
});
});

describe('useString tests', () => {
describe('getString', () => {
test('works with simple id', () => {
const wrapper = ({ children }: { children: React.ReactElement }): React.ReactElement => (
<StringsContext.Provider value={value as any}>{children}</StringsContext.Provider>
);
const { result } = renderHook(() => useStrings(), { wrapper });

expect(result.current.getString('chaos' as any)).toMatchInlineSnapshot(`"Chaos"`);
});

test('works with nested values', () => {
const wrapper = ({ children }: { children: React.ReactElement }): React.ReactElement => (
<StringsContext.Provider value={value as any}>{children}</StringsContext.Provider>
);
const { result } = renderHook(() => useStrings(), { wrapper });

expect(result.current.getString('a.b' as any)).toMatchInlineSnapshot(`"Test Value 1"`);
});

test('works with self reference values', () => {
const wrapper = ({ children }: { children: React.ReactElement }): React.ReactElement => (
<StringsContext.Provider value={value as any}>{children}</StringsContext.Provider>
);
const { result } = renderHook(() => useStrings(), { wrapper });

expect(result.current.getString('test' as any)).toMatchInlineSnapshot(`"Test Value 1 in template"`);
});

test('self reference works foor only one level', () => {
const wrapper = ({ children }: { children: React.ReactElement }): React.ReactElement => (
<StringsContext.Provider value={value as any}>{children}</StringsContext.Provider>
);
const { result } = renderHook(() => useStrings(), { wrapper });

expect(result.current.getString('test2' as any)).toMatchInlineSnapshot(`"{{ $.a.b }} in template again"`);
});
});

test('Works with custom getString', () => {
const { container } = render(
<StringsContext.Provider value={{ ...value, getString: (key: string) => key } as any}>
<String stringID={'chaos.foo.bar.baz' as any} />
</StringsContext.Provider>
);

expect(container).toMatchInlineSnapshot(`
<div>
<span>
chaos.foo.bar.baz
</span>
</div>
`);
});
});
4 changes: 4 additions & 0 deletions chaoscenter/web/src/strings/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { useStrings, String } from './String';
export type { UseStringsReturn } from './String';
export { useStringsContext, StringsContext } from './StringsContext';
export type { StringKeys } from './StringsContext';
1 change: 1 addition & 0 deletions chaoscenter/web/src/strings/strings.en.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
litmus: litmus
Loading

0 comments on commit f886169

Please sign in to comment.