Skip to content

Commit 3c3ddf3

Browse files
authored
Add Translation.textOptional() to support common translation string defaults: (#140)
* Make `Translation` generic with a type parameter for a translation key to allow typed keys when used externally; * Add `Translation.textOptional()` to support common translation string defaults with ability to provide separate string for each case; * Use `search_defaults.input.placeholder` for a search input field; * Use `visual_authoring.dialog.apply.{label, text}` for an "Apply" button in `VisualAuthoring` dialogs.
1 parent fb93a26 commit 3c3ddf3

15 files changed

Lines changed: 100 additions & 33 deletions

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1818
- Support conditionally rendering `WorkspaceLayout*` child components by passing `null` instead of a child to remove it:
1919
* Allow to hide left and right panels in `ClassicWorkspace` by passing `null` to the corresponding props;
2020
* Allow to provide `className` and `style` to `WorkspaceLayout*` components.
21+
- Add `Translation.textOptional()` to support common translation string defaults with ability to provide separate string for each case:
22+
* Use `search_defaults.input.placeholder` for a search input field;
23+
* Use `visual_authoring.dialog.apply.{label, text}` for an "Apply" button in `VisualAuthoring` dialogs.
2124

2225
#### 💅 Polish
2326
- Expose `useAsync()` utility hook to simplify data loading from via a single Promise-returning task.
2427
- Provide `onlySelected` property to link templates the same way as for element templates.
2528
- Allow to configure whether `ClassTree` and `SearchSectionElementTypes` tree items should be draggable.
2629
- Allow to configure `SparqlDataProvider.lookup()` via `lookupQuery` and `filterInnerPrelude` settings.
27-
- Add separate translation keys for input placeholders on every search input field.
2830

2931
#### 🔧 Maintenance
3032
- Preparations to extract generic scrollable paper component `Paper` from diagram-specific state and logic:

i18n/i18n.schema.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
33
"$defs": {
44
"Group": { "type": "object" },
5-
"Value": { "type": "string" }
5+
"Value": { "type": ["string", "null"] }
66
},
77
"type": "object",
88
"additionalProperties": false,
@@ -384,14 +384,20 @@
384384
"dialog.apply.title": { "$ref": "#/$defs/Value" },
385385
"dialog.cancel.label": { "$ref": "#/$defs/Value" },
386386
"dialog.cancel.title": { "$ref": "#/$defs/Value" },
387+
"edit_entity.dialog.apply.label": { "$ref": "#/$defs/Value" },
388+
"edit_entity.dialog.apply.title": { "$ref": "#/$defs/Value" },
387389
"edit_entity.dialog.caption": { "$ref": "#/$defs/Value" },
388390
"edit_entity.iri.label": { "$ref": "#/$defs/Value" },
389391
"edit_entity.label.label": { "$ref": "#/$defs/Value" },
392+
"edit_relation.dialog.apply.label": { "$ref": "#/$defs/Value" },
393+
"edit_relation.dialog.apply.title": { "$ref": "#/$defs/Value" },
390394
"edit_relation.dialog.caption": { "$ref": "#/$defs/Value" },
391395
"edit_relation.dialog.caption_new": { "$ref": "#/$defs/Value" },
392396
"edit_relation.validation_progress.title": { "$ref": "#/$defs/Value" },
393397
"find_or_create.connect_command": { "$ref": "#/$defs/Value" },
394398
"find_or_create.create_command": { "$ref": "#/$defs/Value" },
399+
"find_or_create.dialog.apply.label": { "$ref": "#/$defs/Value" },
400+
"find_or_create.dialog.apply.title": { "$ref": "#/$defs/Value" },
395401
"find_or_create.dialog.caption": { "$ref": "#/$defs/Value" },
396402
"find_or_create.loading.label": { "$ref": "#/$defs/Value" },
397403
"find_or_create.validation_progress.title": { "$ref": "#/$defs/Value" },
@@ -400,6 +406,8 @@
400406
"property.remove_value.title": { "$ref": "#/$defs/Value" },
401407
"property.text_value.placeholder": { "$ref": "#/$defs/Value" },
402408
"property.title": { "$ref": "#/$defs/Value" },
409+
"rename_link.dialog.apply.label": { "$ref": "#/$defs/Value" },
410+
"rename_link.dialog.apply.title": { "$ref": "#/$defs/Value" },
403411
"rename_link.dialog.caption": { "$ref": "#/$defs/Value" },
404412
"rename_link.label.label": { "$ref": "#/$defs/Value" },
405413
"select_entity.entity_type.label": { "$ref": "#/$defs/Value" },

i18n/translations/en.reactodia-translation.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"entities.no_results": "No results",
5757
"entities.truncated_results": "Only the first {{limit}} entities are shown",
5858
"entities.truncated_results_expand": "Only the first {{limit}} entities are shown,\nclick here for the full results",
59-
"input.placeholder": "Search for...",
59+
"input.placeholder": null,
6060
"link.both_navigate_title": "Navigate to all connected elements",
6161
"link.both_title": "all connected",
6262
"link.source_navigate_title": "Navigate to source \"{{relation}}\" elements",
@@ -160,7 +160,7 @@
160160
},
161161
"search_element_types": {
162162
"drag_create.title": "Click or drag to create new entity of this type",
163-
"input.placeholder": "Search for...",
163+
"input.placeholder": null,
164164
"no_results": "No element types found.",
165165
"refresh_progress.title": "Loading element type tree",
166166
"show_only_creatable": "Show only constructible"
@@ -175,7 +175,7 @@
175175
"criteria_connected_via": "Connected to {{entity}} through {{relationType}}",
176176
"criteria_connected_to_source": "Connected to {{entity}} through {{relationType}} as {{sourceIcon}}\u00A0source",
177177
"criteria_connected_to_target": "Connected to {{entity}} through {{relationType}} as {{targetIcon}}\u00A0target",
178-
"input.placeholder": "Search for...",
178+
"input.placeholder": null,
179179
"place_elements.command": "Add selected elements",
180180
"query_progress.title": "Querying for entities",
181181
"show_more_results.label": "Show more",
@@ -186,7 +186,7 @@
186186
"heading_connected": "Connected to selected entities",
187187
"heading_connected_single": "Connected to {{entity}}",
188188
"heading_other": "Other links",
189-
"input.placeholder": "Search for...",
189+
"input.placeholder": null,
190190
"no_results": "No links found.",
191191
"switch.command": "Change link types visibility",
192192
"switch_all.label": "Switch all",
@@ -264,7 +264,7 @@
264264
"undo.title_named": "Undo: {{command}}"
265265
},
266266
"unified_search": {
267-
"input.placeholder": "Search for...",
267+
"input.placeholder": null,
268268
"input_clear.title": "Clear",
269269
"input_collapse.title": "Close"
270270
},
@@ -273,13 +273,19 @@
273273
"dialog.apply.title": "Apply changes",
274274
"dialog.cancel.label": "Cancel",
275275
"dialog.cancel.title": "Cancel the dialog",
276+
"edit_entity.dialog.apply.label": null,
277+
"edit_entity.dialog.apply.title": null,
276278
"edit_entity.dialog.caption": "Edit entity",
277279
"edit_entity.iri.label": "IRI",
280+
"edit_relation.dialog.apply.label": null,
281+
"edit_relation.dialog.apply.title": null,
278282
"edit_relation.dialog.caption": "Edit relation",
279283
"edit_relation.dialog.caption_new": "Establish new relation",
280284
"edit_relation.validation_progress.title": "Validating selected link type",
281285
"find_or_create.connect_command": "Link to an entity",
282286
"find_or_create.create_command": "Create new entity",
287+
"find_or_create.dialog.apply.label": null,
288+
"find_or_create.dialog.apply.title": null,
283289
"find_or_create.dialog.caption": "Establish New Connection",
284290
"find_or_create.loading.label": "Loading entity...",
285291
"find_or_create.validation_progress.title": "Validating selected element and link types",
@@ -288,10 +294,12 @@
288294
"property.remove_value.title": "Remove the property value",
289295
"property.text_value.placeholder": "Property value",
290296
"property.title": "{{property}} {{propertyIri}}",
297+
"rename_link.dialog.apply.label": null,
298+
"rename_link.dialog.apply.title": null,
291299
"rename_link.dialog.caption": "Rename Link",
292300
"rename_link.label.label": "Label",
293301
"select_entity.entity_type.label": "{{type}}",
294-
"select_entity.input.placeholder": "Search for...",
302+
"select_entity.input.placeholder": null,
295303
"select_entity.type.label": "Entity Type",
296304
"select_entity.type.placeholder": "Select entity type",
297305
"select_entity.results.aria_label": "Select an existing element to put on a diagram",

src/coreUtils/i18n.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,34 @@ export type TranslationKey = TranslationKeyOf<DefaultBundleData>;
3838
*
3939
* @category Core
4040
*/
41-
export interface Translation {
41+
export interface Translation<K extends string = TranslationKey> {
4242
/**
4343
* Formats a translation string by replacing placeholders with
4444
* provided values.
4545
*/
4646
text(
47-
key: TranslationKey,
47+
key: K,
4848
placeholders?: Record<string, string | number | boolean>
4949
): string;
5050

51+
/**
52+
* Formats a translation string by replacing placeholders with
53+
* provided values if the string exists.
54+
*
55+
* Returns `undefined` if a translation string is `null` or does exists
56+
* for the specified `key`.
57+
*/
58+
textOptional(
59+
key: K,
60+
placeholders?: Record<string, string | number | boolean>
61+
): string | undefined;
62+
5163
/**
5264
* Templates a translation string into React Fragment by replacing
5365
* placeholders with provided React nodes (elements, etc).
5466
*/
5567
template(
56-
key: TranslationKey,
68+
key: K,
5769
parts: Record<string, React.ReactNode>
5870
): React.ReactNode;
5971

@@ -201,7 +213,7 @@ type DeepPath<T> = T extends object ? (
201213
*/
202214
export class TranslatedText {
203215
private constructor(
204-
private readonly key: TranslationKey,
216+
private readonly key: string,
205217
private readonly placeholders: Record<string, string | number | boolean> | undefined
206218
) {}
207219

@@ -221,7 +233,7 @@ export class TranslatedText {
221233
/**
222234
* Resolves a translation string referenced by the current instance.
223235
*/
224-
resolve(translation: Translation): string {
236+
resolve(translation: Translation<any>): string {
225237
return translation.text(this.key, this.placeholders);
226238
}
227239
}

src/diagram/locale.tsx

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,39 @@ export class DefaultTranslation implements Translation {
1717
protected readonly selectLabelLanguage: LabelLanguageSelector = defaultSelectLabel
1818
) {}
1919

20-
private getString(key: TranslationKey): string {
20+
private getString(key: TranslationKey): string | undefined {
2121
const dotIndex = key.indexOf('.');
2222
if (!(dotIndex > 0 && dotIndex < key.length)) {
2323
throw new Error(`Reactodia: Invalid translation key: ${key}`);
2424
}
2525
const group = key.substring(0, dotIndex);
2626
const leaf = key.substring(dotIndex + 1);
2727
for (const bundle of this.bundles) {
28-
const text = getString(bundle, group, leaf);
29-
if (text !== undefined) {
28+
const text = lookupString(bundle, group, leaf);
29+
if (!(text === null || text === undefined)) {
3030
return text;
3131
}
3232
}
33-
return key;
33+
return undefined;
3434
}
3535

3636
text(key: TranslationKey, placeholders?: Record<string, string | number | boolean>): string {
37+
return this.textOptional(key, placeholders) ?? key;
38+
}
39+
40+
textOptional(
41+
key: TranslationKey,
42+
placeholders?: Record<string, string | number | boolean>
43+
): string | undefined {
3744
const template = this.getString(key);
45+
if (template === undefined) {
46+
return undefined;
47+
}
3848
return formatPlaceholders(template, placeholders);
3949
}
4050

4151
template(key: TranslationKey, parts: Record<string, React.ReactNode>): React.ReactNode {
42-
const template = this.getString(key);
52+
const template = this.getString(key) ?? key;
4353
return templatePlaceholders(template, parts);
4454
}
4555

@@ -79,7 +89,7 @@ export class DefaultTranslation implements Translation {
7989
}
8090
}
8191

82-
function getString(
92+
function lookupString(
8393
bundle: Partial<TranslationBundle>,
8494
group: string,
8595
leaf: string

src/forms/editEntityForm.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,15 @@ export function EditEntityForm(props: {
108108
<div className={`${FORM_CLASS}__controls`}>
109109
<button type='button'
110110
className={`reactodia-btn reactodia-btn-primary ${FORM_CLASS}__apply-button`}
111-
title={t.text('visual_authoring.dialog.apply.title')}
111+
title={
112+
t.textOptional('visual_authoring.edit_entity.dialog.apply.title') ??
113+
t.text('visual_authoring.dialog.apply.title')
114+
}
112115
onClick={() => onApply(data)}>
113-
{t.text('visual_authoring.dialog.apply.label')}
116+
{(
117+
t.textOptional('visual_authoring.edit_entity.dialog.apply.label') ??
118+
t.text('visual_authoring.dialog.apply.label')
119+
)}
114120
</button>
115121
<button type='button'
116122
className='reactodia-btn reactodia-btn-secondary'

src/forms/editRelationForm.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,14 @@ export function DefaultEditRelationForm(props: DefaultEditRelationFormProps) {
257257
<button className={`reactodia-btn reactodia-btn-primary ${FORM_CLASS}__apply-button`}
258258
onClick={onApply}
259259
disabled={status !== 'ok'}
260-
title={t.text('visual_authoring.dialog.apply.title')}>
261-
{t.text('visual_authoring.dialog.apply.label')}
260+
title={
261+
t.textOptional('visual_authoring.edit_relation.dialog.apply.title') ??
262+
t.text('visual_authoring.dialog.apply.title')
263+
}>
264+
{(
265+
t.textOptional('visual_authoring.edit_relation.dialog.apply.label') ??
266+
t.text('visual_authoring.dialog.apply.label')
267+
)}
262268
</button>
263269
<button className='reactodia-btn reactodia-btn-secondary'
264270
onClick={closeDialog}

src/forms/elementTypeSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ export class ElementTypeSelectorInner extends React.Component<ElementTypeSelecto
309309
inputProps={{
310310
className: `${CLASS_NAME}__search-input`,
311311
name: 'reactodia-element-type-selector-search',
312-
placeholder: t.text('visual_authoring.select_entity.input.placeholder'),
312+
placeholder: t.textOptional('visual_authoring.select_entity.input.placeholder'),
313313
autoFocus: true,
314314
}}>
315315
<span className={`${CLASS_NAME}__search-icon`} />

src/forms/findOrCreateEntityForm.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,14 @@ export class FindOrCreateEntityForm extends React.Component<FindOrCreateEntityFo
160160
<button className={`reactodia-btn reactodia-btn-primary ${FORM_CLASS}__apply-button`}
161161
onClick={this.onApply}
162162
disabled={elementValue.loading || !isValid || isValidating}
163-
title={t.text('visual_authoring.dialog.apply.title')}>
164-
{t.text('visual_authoring.dialog.apply.label')}
163+
title={
164+
t.textOptional('visual_authoring.find_or_create.dialog.apply.title') ??
165+
t.text('visual_authoring.dialog.apply.title')
166+
}>
167+
{(
168+
t.textOptional('visual_authoring.find_or_create.dialog.apply.label') ??
169+
t.text('visual_authoring.dialog.apply.label')
170+
)}
165171
</button>
166172
<button className='reactodia-btn reactodia-btn-secondary'
167173
onClick={this.props.onCancel}

src/forms/renameLinkForm.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ export function RenameLinkForm(props: RenameLinkFormProps) {
4747
</div>
4848
<div className={`${FORM_CLASS}__controls`}>
4949
<button className={`reactodia-btn reactodia-btn-primary ${FORM_CLASS}__apply-button`}
50-
title={t.text('visual_authoring.dialog.apply.title')}
50+
title={
51+
t.textOptional('visual_authoring.rename_link.dialog.apply.title') ??
52+
t.text('visual_authoring.dialog.apply.title')
53+
}
5154
onClick={onApply}>
52-
{t.text('visual_authoring.dialog.apply.label')}
55+
{(
56+
t.textOptional('visual_authoring.rename_link.dialog.apply.label') ??
57+
t.text('visual_authoring.dialog.apply.label')
58+
)}
5359
</button>
5460
<button className='reactodia-btn reactodia-btn-secondary'
5561
title={t.text('visual_authoring.dialog.cancel.title')}

0 commit comments

Comments
 (0)