Skip to content

Commit 0b316e5

Browse files
First pass on labels with itext translations!
1 parent fcff2ab commit 0b316e5

File tree

22 files changed

+382
-149
lines changed

22 files changed

+382
-149
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?xml version="1.0"?>
2+
<h:html xmlns="http://www.w3.org/2002/xforms"
3+
xmlns:ev="http://www.w3.org/2001/xml-events"
4+
xmlns:h="http://www.w3.org/1999/xhtml"
5+
xmlns:jr="http://openrosa.org/javarosa"
6+
xmlns:orx="http://openrosa.org/xforms/"
7+
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
8+
<h:head>
9+
<h:title>Itext labels</h:title>
10+
<model>
11+
<itext>
12+
<translation lang="English">
13+
<text id="f1">
14+
<value>1. Field one</value>
15+
</text>
16+
<text id="g1">
17+
<value>2. Group one</value>
18+
</text>
19+
<text id="g1f">
20+
<value>2.1. First field in group one</value>
21+
</text>
22+
</translation>
23+
<translation lang="French">
24+
<text id="f1">
25+
<value>1. Champ un</value>
26+
</text>
27+
<text id="g1">
28+
<value>2. Groupe un</value>
29+
</text>
30+
<text id="g1f">
31+
<value>2.1. Premier champ du groupe un</value>
32+
</text>
33+
</translation>
34+
</itext>
35+
<instance>
36+
<root id="itext-labels">
37+
<field-1 />
38+
<group-1>
39+
<group-1-field-1 />
40+
</group-1>
41+
</root>
42+
</instance>
43+
<bind nodeset="/root/field-1" />
44+
<bind nodeset="/root/group-1" />
45+
<bind nodeset="/root/group-1/group-1-field-1" />
46+
</model>
47+
</h:head>
48+
<h:body>
49+
<input ref="/root/field-1">
50+
<label ref="jr:itext('f1')" />
51+
</input>
52+
<group ref="/root/group-1">
53+
<label ref="jr:itext('g1')" />
54+
55+
<input ref="/root/group-1/group-1-field-1">
56+
<label ref="jr:itext('g1f')" />
57+
</input>
58+
</group>
59+
</h:body>
60+
</h:html>

packages/odk-web-forms/src/App.tsx

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
1-
import { Show, Suspense, createEffect, createResource, createSignal, on } from 'solid-js';
1+
import {
2+
Show,
3+
Suspense,
4+
createEffect,
5+
createMemo,
6+
createResource,
7+
createSignal,
8+
on,
9+
untrack,
10+
} from 'solid-js';
211
import { Divider, Stack } from 'suid/material';
312
import { DemoFixturesList, type SelectedDemoFixture } from './components/Demo/DemoFixturesList.tsx';
4-
import { LocalizationProvider } from './components/LocalizationProvider.tsx';
513
import { Page } from './components/Page/Page.tsx';
614
import { ThemeProvider } from './components/ThemeProvider.tsx';
715
import { XFormDetails } from './components/XForm/XFormDetails.tsx';
816
import { XFormView } from './components/XForm/XFormView.tsx';
9-
import type { Localization } from './lib/i18n-l10n/types.ts';
1017
import { XFormDefinition } from './lib/xform/XFormDefinition.ts';
1118
import { XFormEntry } from './lib/xform/XFormEntry.ts';
1219

13-
// TODO: this is just to populate the menu for now
14-
const localizations: readonly Localization[] = [
15-
{
16-
locale: 'en-us',
17-
name: 'English (US)',
18-
},
19-
{
20-
locale: 'es',
21-
name: 'Spanish',
22-
},
23-
];
24-
2520
export const App = () => {
2621
const [fixture, setFixture] = createSignal<SelectedDemoFixture | null>(null);
2722
// A resource (Solid's mechanism for data fetching and triggering Suspense) is a
@@ -31,8 +26,23 @@ export const App = () => {
3126
// TODO: more fixtures are likely incoming rather soon, it'll make sense to have
3227
// an app entry to correspond to that, and allow selection of particular fixtures,
3328
// perhaps arbitrary forms as well.
34-
const [fixtureSourceXML, { refetch }] = createResource(async () => {
35-
return await Promise.resolve(fixture()?.xml);
29+
const [fixtureSourceXML, { refetch }] = createResource(() => {
30+
return fixture()?.xml ?? null;
31+
});
32+
const formInit = createMemo(() => {
33+
const sourceXML = fixtureSourceXML();
34+
35+
if (sourceXML == null) {
36+
return null;
37+
}
38+
39+
const definition = new XFormDefinition(sourceXML);
40+
const entry = untrack(() => new XFormEntry(definition));
41+
42+
return {
43+
definition,
44+
entry,
45+
};
3646
});
3747

3848
createEffect(
@@ -43,30 +53,25 @@ export const App = () => {
4353

4454
return (
4555
<ThemeProvider>
46-
<LocalizationProvider localizations={localizations}>
47-
<Page>
48-
<DemoFixturesList setDemoFixture={setFixture} />
49-
<Suspense fallback={<p>Loading…</p>}>
50-
<Show when={fixtureSourceXML()} keyed={true}>
51-
{(sourceXML) => {
52-
const definition = new XFormDefinition(sourceXML);
53-
const entry = new XFormEntry(definition);
54-
55-
return (
56-
<Stack spacing={4}>
56+
<Page entry={formInit()?.entry ?? null}>
57+
<DemoFixturesList setDemoFixture={setFixture} />
58+
<Suspense fallback={<p>Loading…</p>}>
59+
<Show when={formInit()} keyed={true}>
60+
{({ definition, entry }) => {
61+
return (
62+
<Stack spacing={4}>
63+
<Divider />
64+
<Stack spacing={7}>
65+
<XFormView entry={entry} />
5766
<Divider />
58-
<Stack spacing={7}>
59-
<XFormView entry={entry} />
60-
<Divider />
61-
<XFormDetails definition={definition} entry={entry} />
62-
</Stack>
67+
<XFormDetails definition={definition} entry={entry} />
6368
</Stack>
64-
);
65-
}}
66-
</Show>
67-
</Suspense>
68-
</Page>
69-
</LocalizationProvider>
69+
</Stack>
70+
);
71+
}}
72+
</Show>
73+
</Suspense>
74+
</Page>
7075
</ThemeProvider>
7176
);
7277
};

packages/odk-web-forms/src/components/FormLanguageMenu.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// TODO: lots of this should get broken out
22

3-
import { createSignal, useContext } from 'solid-js';
3+
import { createSignal } from 'solid-js';
44
import { For, Show } from 'solid-js/web';
55
import Check from 'suid/icons-material/Check';
66
import ExpandMore from 'suid/icons-material/ExpandMore';
@@ -14,7 +14,7 @@ import {
1414
Typography,
1515
styled,
1616
} from 'suid/material';
17-
import { localizationContext } from './LocalizationProvider.tsx';
17+
import type { XFormEntry } from '../lib/xform/XFormEntry.ts';
1818
import { PageMenuButton } from './styled/PageMenuButton.tsx';
1919

2020
const FormLanguageMenuButtonIcon = styled(Language)(({ theme }) => ({
@@ -29,19 +29,23 @@ const MenuItemSmallTypography = styled(Typography)({
2929
fontSize: '0.875rem',
3030
});
3131

32-
export const FormLanguageMenu = () => {
32+
interface FormLanguageMenuProps {
33+
readonly entry: XFormEntry | null;
34+
}
35+
36+
export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
3337
let buttonRef: HTMLButtonElement;
3438

35-
const context = useContext(localizationContext);
3639
const [isOpen, setIsOpen] = createSignal(false);
37-
const [selected, setSelected] = createSignal(context.localizations[0] ?? null);
3840
const closeMenu = () => {
3941
setIsOpen(false);
4042
};
4143

4244
return (
43-
<Show when={selected()} keyed={true}>
44-
{(currentLocalization) => {
45+
<Show when={props.entry?.isTranslated && props.entry} keyed={true}>
46+
{(entry) => {
47+
const currentLanguage = () => entry.getCurrentLanguage();
48+
4549
return (
4650
<div>
4751
<PageMenuButton
@@ -57,7 +61,7 @@ export const FormLanguageMenu = () => {
5761
>
5862
<Stack alignItems="center" direction="row">
5963
<FormLanguageMenuButtonIcon fontSize="small" />
60-
<span style={{ 'line-height': 1 }}>{currentLocalization.name}</span>
64+
<span style={{ 'line-height': 1 }}>{currentLanguage()}</span>
6165
<FormLanguageMenuExpandMoreIcon fontSize="small" />
6266
</Stack>
6367
</PageMenuButton>
@@ -82,16 +86,16 @@ export const FormLanguageMenu = () => {
8286
horizontal: 'right',
8387
}}
8488
>
85-
<For each={context.localizations}>
86-
{(localization) => {
87-
const isSelected = () => localization === selected();
89+
<For each={entry.getLanguages()}>
90+
{(language) => {
91+
const isSelected = () => language === currentLanguage();
8892

8993
return (
9094
<MenuItem
9195
dense={true}
9296
selected={isSelected()}
9397
onClick={() => {
94-
setSelected(localization);
98+
entry.setCurrentLanguage(language);
9599
closeMenu();
96100
}}
97101
>
@@ -102,7 +106,7 @@ export const FormLanguageMenu = () => {
102106
</Show>
103107

104108
<ListItemText inset={!isSelected()} disableTypography={true}>
105-
<MenuItemSmallTypography>{localization.name}</MenuItemSmallTypography>
109+
<MenuItemSmallTypography>{language}</MenuItemSmallTypography>
106110
</ListItemText>
107111
</MenuItem>
108112
);

packages/odk-web-forms/src/components/Page/Page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { JSX } from 'solid-js';
22
import { GlobalStyles, Stack, useTheme } from 'suid/material';
3+
import type { XFormEntry } from '../../lib/xform/XFormEntry.ts';
34
import { PageContainer } from '../styled/PageContainer.tsx';
45
import { PageFooter } from './PageFooter.tsx';
56
import { PageHeader } from './PageHeader.tsx';
67
import { PageMain } from './PageMain.tsx';
78

89
interface PageProps {
910
readonly children?: JSX.Element;
11+
readonly entry: XFormEntry | null;
1012
}
1113

1214
export const Page = (props: PageProps) => {
@@ -36,7 +38,7 @@ export const Page = (props: PageProps) => {
3638
/>
3739
<PageContainer>
3840
<Stack spacing={2}>
39-
<PageHeader />
41+
<PageHeader entry={props.entry} />
4042
<PageMain elevation={2}>{props.children}</PageMain>
4143
<PageFooter />
4244
</Stack>
Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import { Stack } from 'suid/material';
2+
import type { XFormEntry } from '../../lib/xform/XFormEntry.ts';
23
import { FormLanguageMenu } from '../FormLanguageMenu.tsx';
34

4-
export const PageHeader = () => {
5+
interface PageHeaderProps {
6+
readonly entry: XFormEntry | null;
7+
}
8+
9+
export const PageHeader = (props: PageHeaderProps) => {
510
return (
611
<Stack direction="row" justifyContent="flex-end">
7-
<FormLanguageMenu />
12+
<FormLanguageMenu entry={props.entry} />
813
</Stack>
914
);
1015
};

packages/odk-web-forms/src/components/Widget/TextWidget.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Show, createMemo, createUniqueId } from 'solid-js';
2+
import type { XFormEntry } from '../../lib/xform/XFormEntry.ts';
23
import type { XFormEntryBinding } from '../../lib/xform/XFormEntryBinding.ts';
34
import type { XFormViewLabel } from '../../lib/xform/XFormViewLabel.ts';
45
import { XFormControlLabel } from '../XForm/controls/XFormControlLabel.tsx';
@@ -9,6 +10,7 @@ export interface TextWidgetProps {
910
readonly label: XFormViewLabel | null;
1011
readonly ref: string | null;
1112
readonly binding: XFormEntryBinding | null;
13+
readonly entry: XFormEntry;
1214
}
1315

1416
export const TextWidget = (props: TextWidgetProps) => {
@@ -25,7 +27,9 @@ export const TextWidget = (props: TextWidgetProps) => {
2527
<DefaultTextFormControl fullWidth={true}>
2628
<Show when={props.label} keyed={true}>
2729
{(label) => {
28-
return <XFormControlLabel id={id} binding={binding} label={label} />;
30+
return (
31+
<XFormControlLabel id={id} binding={binding} entry={props.entry} label={label} />
32+
);
2933
}}
3034
</Show>
3135
<DefaultTextField

packages/odk-web-forms/src/components/XForm/XFormLabel.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Show } from 'solid-js';
1+
import { For, Show, createMemo } from 'solid-js';
2+
import type { XFormEntry } from '../../lib/xform/XFormEntry';
23
import type { XFormEntryBinding } from '../../lib/xform/XFormEntryBinding';
34
import type { XFormViewLabel } from '../../lib/xform/XFormViewLabel';
45
import { DefaultLabel } from '../styled/DefaultLabel';
@@ -7,18 +8,23 @@ import { DefaultLabelRequiredIndicator } from '../styled/DefaultLabelRequiredInd
78
export interface XFormLabelProps {
89
readonly as?: 'span';
910
readonly binding: XFormEntryBinding;
11+
readonly entry: XFormEntry;
1012
readonly id: string;
1113
readonly label: XFormViewLabel;
1214
}
1315

1416
export const XFormLabel = (props: XFormLabelProps) => {
17+
const labelParts = createMemo(() => {
18+
return props.label.parts.map((part) => part.evaluate(props.binding));
19+
});
20+
1521
return (
1622
<>
1723
<Show when={props.binding.isRequired()}>
1824
<DefaultLabelRequiredIndicator>* </DefaultLabelRequiredIndicator>
1925
</Show>
2026
<DefaultLabel as={props.as ?? 'label'} for={props.id}>
21-
{props.label.labelText}
27+
<For each={labelParts()}>{(part) => part}</For>
2228
</DefaultLabel>
2329
</>
2430
);

0 commit comments

Comments
 (0)