Skip to content

Commit d5d4840

Browse files
committed
Support external workspace root and translation context:
* Allow to define `WorkspaceRoot` and `TranslationProvider` as parent to the `Workspace` component so they can be shared between multiple workspaces or used outside the workspace itself. * Deprecate `translations`, `useDefaultTranslation` and `selectLabelLanguage` props on `Workspace` component in favor of wrapping the workspace in `TranslationProvider` with `DefaultTranslation`. * Remove deprecated `Translation.formatIri()` method (use `DataLocaleProvider.formatIri()` instead).
1 parent 65f1f11 commit d5d4840

8 files changed

Lines changed: 151 additions & 97 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1616
- Fix canvas panning optimization not being applied due to incorrect `z-index` value.
1717

1818
#### 💅 Polish
19+
- Allow to define `WorkspaceRoot` and `TranslationProvider` as parent to the `Workspace` component so they can be shared between multiple workspaces or used outside the workspace itself:
20+
* Deprecate `translations`, `useDefaultTranslation` and `selectLabelLanguage` props on `Workspace` component in favor of wrapping the workspace in `TranslationProvider` with (newly) exported `DefaultTranslation`;
21+
* Remove deprecated `Translation.formatIri()` method (use `DataLocaleProvider.formatIri()` instead).
1922
- Allow to configure `SearchResults` utility component with `isItemDisabled` and `multiSelection` props:
2023
* Remove `singleSelectOnClick` mode from `SearchResults` as it mostly superseded by `multiSelection`.
2124
- Extend `ListElementView` utility component to accept any other additional HTML props.

examples/i18n.tsx

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ const Layouts = Reactodia.defineLayoutWorker(() => new Worker(
1515
function I18nExample() {
1616
const {defaultLayout} = Reactodia.useWorker(Layouts);
1717

18+
const translation = React.useMemo(() => new Reactodia.DefaultTranslation({
19+
bundles: [
20+
{
21+
'default_workspace': {
22+
'search_section_entities.label': 'Nodes',
23+
'search_section_entities.title': 'Graph Nodes Lookup',
24+
'search_section_entity_types.label': 'Node Types',
25+
'search_section_entity_types.title': 'Graph Node Type Hierarchy',
26+
'search_section_link_types.label': 'Edge Types',
27+
'search_section_link_types.title': 'Graph Edge Types on the diagram'
28+
},
29+
'search_defaults': {
30+
'input_term_too_short': 'Minimum search term length is {{termLength}}',
31+
},
32+
'search_entities': {
33+
'criteria_connected_to_source':
34+
'{{sourceIcon}}\u00A0{{entity}} (source) via {{relationType}}',
35+
'criteria_connected_to_target':
36+
'{{targetIcon}}\u00A0{{entity}} (target) via {{relationType}}',
37+
},
38+
'toolbar_action': {
39+
'layout.label': 'Layout the graph',
40+
},
41+
}
42+
]
43+
}), []);
44+
1845
const {onMount} = Reactodia.useLoadedWorkspace(async ({context, signal}) => {
1946
const {model} = context;
2047

@@ -31,36 +58,14 @@ function I18nExample() {
3158
}, []);
3259

3360
return (
34-
<Reactodia.Workspace ref={onMount}
35-
translations={[
36-
{
37-
'default_workspace': {
38-
'search_section_entities.label': 'Nodes',
39-
'search_section_entities.title': 'Graph Nodes Lookup',
40-
'search_section_entity_types.label': 'Node Types',
41-
'search_section_entity_types.title': 'Graph Node Type Hierarchy',
42-
'search_section_link_types.label': 'Edge Types',
43-
'search_section_link_types.title': 'Graph Edge Types on the diagram'
44-
},
45-
'search_defaults': {
46-
'input_term_too_short': 'Minimum search term length is {{termLength}}',
47-
},
48-
'search_entities': {
49-
'criteria_connected_to_source':
50-
'{{sourceIcon}}\u00A0{{entity}} (source) via {{relationType}}',
51-
'criteria_connected_to_target':
52-
'{{targetIcon}}\u00A0{{entity}} (target) via {{relationType}}',
53-
},
54-
'toolbar_action': {
55-
'layout.label': 'Layout the graph',
56-
},
57-
}
58-
]}
59-
defaultLayout={defaultLayout}>
60-
<Reactodia.DefaultWorkspace
61-
menu={<ExampleToolbarMenu />}
62-
/>
63-
</Reactodia.Workspace>
61+
<Reactodia.TranslationProvider translation={translation}>
62+
<Reactodia.Workspace ref={onMount}
63+
defaultLayout={defaultLayout}>
64+
<Reactodia.DefaultWorkspace
65+
menu={<ExampleToolbarMenu />}
66+
/>
67+
</Reactodia.Workspace>
68+
</Reactodia.TranslationProvider>
6469
);
6570
}
6671

src/coreUtils/colorScheme.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react';
22

33
export type ColorScheme = 'light' | 'dark';
44

5-
export const ColorSchemeContext = React.createContext<ColorScheme>('light');
5+
export const ColorSchemeContext = React.createContext<ColorScheme | null>(null);
66

77
/**
88
* React hook to get currently active color scheme for the UI components.
@@ -11,13 +11,15 @@ export const ColorSchemeContext = React.createContext<ColorScheme>('light');
1111
* @see {@link WorkspaceRootProps.colorScheme}
1212
*/
1313
export function useColorScheme(): 'light' | 'dark' {
14-
return React.useContext(ColorSchemeContext);
14+
return React.useContext(ColorSchemeContext) ?? 'light';
1515
}
1616

1717
export interface ColorSchemeApi {
18+
readonly defined: boolean;
1819
readonly actInColorScheme: (scheme: ColorScheme, action: () => void) => void;
1920
}
2021

2122
export const ColorSchemeApi = React.createContext<ColorSchemeApi>({
23+
defined: false,
2224
actInColorScheme: () => {/* nothing */},
2325
});

src/coreUtils/i18n.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,6 @@ export interface Translation<K extends string = TranslationKey> {
136136
fallbackIri: string,
137137
language: string
138138
): string;
139-
140-
/**
141-
* Formats IRI to display in the UI:
142-
* - usual IRIs are enclosed in `<IRI>`;
143-
* - anonymous element IRIs displayed as `(blank node)`.
144-
*
145-
* @deprecated Use {@link DataLocaleProvider.formatIri} instead.
146-
*/
147-
formatIri(iri: string): string;
148139
}
149140

150141
/**
@@ -180,6 +171,30 @@ export interface TranslatedProperty<Iri> {
180171

181172
export const TranslationContext = React.createContext<Translation | null>(null);
182173

174+
/**
175+
* Provides i18n (translation) context for the UI elements.
176+
*
177+
* @category Components
178+
* @see {@link useTranslation}
179+
*/
180+
export function TranslationProvider(props: {
181+
/**
182+
* Provided i18n implementation.
183+
*/
184+
translation: Translation;
185+
/**
186+
* Component children to render with provided i18n context.
187+
*/
188+
children: React.ReactNode;
189+
}) {
190+
const {translation, children} = props;
191+
return (
192+
<TranslationContext.Provider value={translation}>
193+
{children}
194+
</TranslationContext.Provider>
195+
);
196+
}
197+
183198
/**
184199
* Gets current translation data for the UI elements.
185200
*
@@ -188,7 +203,7 @@ export const TranslationContext = React.createContext<Translation | null>(null);
188203
export function useTranslation(): Translation {
189204
const translation = React.useContext(TranslationContext);
190205
if (!translation) {
191-
throw new Error('Reactodia: missing translation context');
206+
throw new Error('Reactodia: missing <TranslationProvider> context');
192207
}
193208
return translation;
194209
}
@@ -209,7 +224,7 @@ type DeepPath<T> = T extends object ? (
209224
* Represents a lazily-resolved simple or formatted translation string.
210225
*
211226
* @category Core
212-
* @see Translation
227+
* @see {@link Translation}
213228
*/
214229
export class TranslatedText {
215230
private constructor(

src/diagram/locale.tsx

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,55 @@ import * as React from 'react';
33
import DefaultBundle from '../../i18n/translations/en.reactodia-translation.json';
44

55
import {
6-
LabelLanguageSelector, Translation, TranslationKey, TranslationBundle, TranslationContext,
6+
LabelLanguageSelector, Translation, TranslationKey, TranslationBundle,
77
} from '../coreUtils/i18n';
88

9-
import { isEncodedBlank } from '../data/model';
109
import * as Rdf from '../data/rdf/rdfModel';
1110

1211
export const DefaultTranslationBundle: TranslationBundle = DefaultBundle;
1312

13+
/**
14+
* Default built-in implementation for i18n strings interpolation and other
15+
* methods from {@link Translation} interface.
16+
*/
1417
export class DefaultTranslation implements Translation {
15-
constructor(
16-
protected readonly bundles: ReadonlyArray<Partial<TranslationBundle>>,
17-
protected readonly selectLabelLanguage: LabelLanguageSelector = defaultSelectLabel
18-
) {}
18+
protected readonly bundles: ReadonlyArray<Partial<TranslationBundle>>;
19+
20+
private readonly _selectLabel: LabelLanguageSelector;
21+
22+
constructor(options: {
23+
/**
24+
* Additional translation bundles for UI text strings in the workspace
25+
* in order from higher to lower priority.
26+
*
27+
* @default []
28+
* @see {@link useDefaultTranslation}
29+
*/
30+
bundles?: ReadonlyArray<Partial<TranslationBundle>>;
31+
/**
32+
* If set, disables translation fallback which (with default `en` language).
33+
*
34+
* @default true
35+
* @see {@link translations}
36+
*/
37+
useDefaultBundle?: boolean;
38+
/**
39+
* Overrides how a single label gets selected from multiple of them based on target language.
40+
*/
41+
selectLabel?: LabelLanguageSelector;
42+
}) {
43+
const {
44+
bundles = [],
45+
useDefaultBundle = true,
46+
selectLabel = defaultSelectLabel,
47+
} = options;
48+
const translationBundles: Partial<TranslationBundle>[] = [...bundles];
49+
if (useDefaultBundle) {
50+
translationBundles.push(DefaultTranslationBundle);
51+
}
52+
this.bundles = translationBundles;
53+
this._selectLabel = selectLabel;
54+
}
1955

2056
private getString(key: TranslationKey): string | undefined {
2157
const dotIndex = key.indexOf('.');
@@ -57,8 +93,8 @@ export class DefaultTranslation implements Translation {
5793
labels: ReadonlyArray<Rdf.Literal>,
5894
language: string
5995
): Rdf.Literal | undefined {
60-
const {selectLabelLanguage} = this;
61-
return selectLabelLanguage(labels, language);
96+
const {_selectLabel} = this;
97+
return _selectLabel(labels, language);
6298
}
6399

64100
selectValues(
@@ -80,13 +116,6 @@ export class DefaultTranslation implements Translation {
80116
const label = labels ? this.selectLabel(labels, language) : undefined;
81117
return resolveLabel(label, fallbackIri);
82118
}
83-
84-
formatIri(iri: string): string {
85-
if (isEncodedBlank(iri)) {
86-
return '(blank node)';
87-
}
88-
return `<${iri}>`;
89-
}
90119
}
91120

92121
function lookupString(
@@ -185,15 +214,3 @@ function resolveLabel(label: Rdf.Literal | undefined, fallbackIri: string): stri
185214
if (label) { return label.value; }
186215
return Rdf.getLocalName(fallbackIri) || fallbackIri;
187216
}
188-
189-
export function TranslationProvider(props: {
190-
translation: Translation;
191-
children: React.ReactNode;
192-
}) {
193-
const {translation, children} = props;
194-
return (
195-
<TranslationContext.Provider value={translation}>
196-
{children}
197-
</TranslationContext.Provider>
198-
);
199-
}

src/workspace.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export {
1515
export type { HotkeyString } from './coreUtils/hotkey';
1616
export {
1717
type LabelLanguageSelector, type TranslatedProperty, TranslatedText, type Translation,
18-
useTranslation,
18+
TranslationProvider, useTranslation,
1919
} from './coreUtils/i18n';
2020
export { KeyedObserver, type KeyedSyncStore, useKeyedSyncStore } from './coreUtils/keyedObserver';
2121
export { Debouncer } from './coreUtils/scheduler';
@@ -102,6 +102,7 @@ export {
102102
LinkVertices, type LinkVerticesProps,
103103
} from './diagram/linkLayer';
104104
export { DefaultLinkRouter, type DefaultLinkRouterOptions } from './diagram/linkRouter';
105+
export { DefaultTranslation } from './diagram/locale';
105106
export { type DiagramModel, type DiagramModelEvents, type GraphStructure } from './diagram/model';
106107
export { CanvasPlaceAt, type CanvasPlaceAtLayer } from './diagram/placeLayer';
107108
export {

0 commit comments

Comments
 (0)