Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial support for itext translations #13

Merged
merged 14 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/common/src/constants/xmlns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const ODK_NAMESPACE_URI = 'http://www.opendatakit.org/xforms';
export const OPENROSA_XFORMS_NAMESPACE_URI = 'http://openrosa.org/xforms';
export const XFORMS_NAMESPACE_URI = 'http://www.w3.org/2002/xforms';

export type JavaRosaNamespaceURI = typeof JAVAROSA_NAMESPACE_URI;
export type XFormsNamespaceURI = typeof XFORMS_NAMESPACE_URI;

// Enketo
export const ENKETO_NAMESPACE_URI = 'http://enketo.org/xforms';

// Default prefixes
export const HTML_PREFIX = 'h';
export const XML_PREFIX = 'xml';
Expand All @@ -21,3 +27,5 @@ export const JAVAROSA_PREFIX = 'jr';
export const ODK_PREFIX = 'odk';
export const OPENROSA_XFORMS_PREFIX = 'orx';
export const XFORMS_PREFIX = 'xf';

export const ENKETO_PREFIX = 'enk';
39 changes: 39 additions & 0 deletions packages/odk-web-forms/fixtures/xforms/itext/01-itext-basic.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms"
xmlns:ev="http://www.w3.org/2001/xml-events"
xmlns:h="http://www.w3.org/1999/xhtml"
xmlns:jr="http://openrosa.org/javarosa"
xmlns:orx="http://openrosa.org/xforms/"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<h:head>
<h:title>Itext (basic)</h:title>
<model>
<itext>
<translation lang="English">
<text id="q1:label">
<value>1. Question one</value>
</text>
</translation>
<translation lang="Español">
<text id="q1:label">
<value>1. Pregunta uno</value>
</text>
</translation>
</itext>
<instance>
<root id="itext-basic">
<q1/>
<meta>
<instanceID/>
</meta>
</root>
</instance>
<bind nodeset="/root/q1"/>
</model>
</h:head>
<h:body>
<input ref="/root/q1">
<label ref="jr:itext('q1:label')" />
</input>
</h:body>
</h:html>
80 changes: 22 additions & 58 deletions packages/odk-web-forms/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,35 @@
import { Show, Suspense, createEffect, createResource, createSignal, on } from 'solid-js';
import { Show, type JSX } from 'solid-js';
import { Divider, Stack } from 'suid/material';
import { DemoFixturesList, type SelectedDemoFixture } from './components/Demo/DemoFixturesList.tsx';
import { LocalizationProvider } from './components/LocalizationProvider.tsx';
import { Page } from './components/Page/Page.tsx';
import { ThemeProvider } from './components/ThemeProvider.tsx';
import { XFormDetails } from './components/XForm/XFormDetails.tsx';
import { XFormView } from './components/XForm/XFormView.tsx';
import type { Localization } from './lib/i18n-l10n/types.ts';
import { XFormDefinition } from './lib/xform/XFormDefinition.ts';
import { EntryState } from './lib/xform/state/EntryState.ts';

// TODO: this is just to populate the menu for now
const localizations: readonly Localization[] = [
{
locale: 'en-us',
name: 'English (US)',
},
{
locale: 'es',
name: 'Spanish',
},
];

export const App = () => {
const [fixture, setFixture] = createSignal<SelectedDemoFixture | null>(null);
// A resource (Solid's mechanism for data fetching and triggering Suspense) is a
// likely way we'll fetch forms, so using it here to anticipate that rather than
// importing the fixture directly.
//
// TODO: more fixtures are likely incoming rather soon, it'll make sense to have
// an app entry to correspond to that, and allow selection of particular fixtures,
// perhaps arbitrary forms as well.
const [fixtureSourceXML, { refetch }] = createResource(async () => {
return await Promise.resolve(fixture()?.xml);
});

createEffect(
on(fixture, async () => {
await refetch();
})
);
interface AppProps {
readonly extras?: JSX.Element;
readonly entry: EntryState | null;
}

export const App = (props: AppProps) => {
return (
<ThemeProvider>
<LocalizationProvider localizations={localizations}>
<Page>
<DemoFixturesList setDemoFixture={setFixture} />
<Suspense fallback={<p>Loading…</p>}>
<Show when={fixtureSourceXML()} keyed={true}>
{(sourceXML) => {
const definition = new XFormDefinition(sourceXML);
const entry = new EntryState(definition);

return (
<Stack spacing={4}>
<Stack spacing={7}>
<XFormView entry={entry} />
<Divider />
<XFormDetails definition={definition} entry={entry} />
</Stack>
</Stack>
);
}}
</Show>
</Suspense>
</Page>
</LocalizationProvider>
<Page entry={props.entry}>
{props.extras}
<Show when={props.entry} keyed={true}>
{(entry) => {
return (
<Stack spacing={4}>
<Stack spacing={7}>
<XFormView entry={entry} />
<Divider />
<XFormDetails entry={entry} />
</Stack>
</Stack>
);
}}
</Show>
</Page>
</ThemeProvider>
);
};
31 changes: 31 additions & 0 deletions packages/odk-web-forms/src/Demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createEffect, createMemo, createResource, createSignal, on, untrack } from 'solid-js';
import { App } from './App.tsx';
import { DemoFixturesList, type SelectedDemoFixture } from './components/Demo/DemoFixturesList.tsx';
import { XFormDefinition } from './lib/xform/XFormDefinition.ts';
import { EntryState } from './lib/xform/state/EntryState.ts';

export const Demo = () => {
const [fixture, setFixture] = createSignal<SelectedDemoFixture | null>(null);
const [fixtureSourceXML, { refetch }] = createResource(async () => {
return await Promise.resolve(fixture()?.xml);
});
const entry = createMemo(() => {
const sourceXML = fixtureSourceXML();

if (sourceXML == null) {
return null;
}

const definition = new XFormDefinition(sourceXML);

return untrack(() => new EntryState(definition));
});

createEffect(
on(fixture, async () => {
await refetch();
})
);

return <App extras={<DemoFixturesList setDemoFixture={setFixture} />} entry={entry()} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export const DemoFixturesList = (props: DemoFixturesListProps) => {
};

const isDemoFixture = (fixtureKey: string) =>
fixtureKey.includes('/computations-demo/') || fixtureKey.includes('/repeats/');
fixtureKey.includes('/computations-demo/') ||
fixtureKey.includes('/repeats/') ||
fixtureKey.includes('/itext/');

const demoFixtures = createMemo(() =>
Array.from(getFixtures().values()).filter(({ key }) => isDemoFixture(key))
Expand Down
151 changes: 75 additions & 76 deletions packages/odk-web-forms/src/components/FormLanguageMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO: lots of this should get broken out

import { createSignal, useContext } from 'solid-js';
import { createSignal } from 'solid-js';
import { For, Show } from 'solid-js/web';
import Check from 'suid/icons-material/Check';
import ExpandMore from 'suid/icons-material/ExpandMore';
Expand All @@ -14,7 +14,7 @@ import {
Typography,
styled,
} from 'suid/material';
import { localizationContext } from './LocalizationProvider.tsx';
import type { TranslationState } from '../lib/xform/state/TranslationState.ts';
import { PageMenuButton } from './styled/PageMenuButton.tsx';

const FormLanguageMenuButtonIcon = styled(Language)(({ theme }) => ({
Expand All @@ -29,89 +29,88 @@ const MenuItemSmallTypography = styled(Typography)({
fontSize: '0.875rem',
});

export const FormLanguageMenu = () => {
interface FormLanguageMenuProps {
readonly translations: TranslationState;
}

export const FormLanguageMenu = (props: FormLanguageMenuProps) => {
let buttonRef: HTMLButtonElement;

const context = useContext(localizationContext);
const [isOpen, setIsOpen] = createSignal(false);
const [selected, setSelected] = createSignal(context.localizations[0] ?? null);
const closeMenu = () => {
setIsOpen(false);
};

return (
<Show when={selected()} keyed={true}>
{(currentLocalization) => {
return (
<div>
<PageMenuButton
id="form-language-menu-button"
ref={buttonRef}
aria-controls={isOpen() ? 'form-language-menu' : ''}
aria-expanded={isOpen()}
aria-aria-haspopup={true}
onClick={() => {
setIsOpen((current) => !current);
}}
variant="contained"
>
<Stack alignItems="center" direction="row">
<FormLanguageMenuButtonIcon fontSize="small" />
<span style={{ 'line-height': 1 }}>{currentLocalization.name}</span>
<FormLanguageMenuExpandMoreIcon fontSize="small" />
</Stack>
</PageMenuButton>
<Menu
id="form-language-menu"
MenuListProps={{ 'aria-labelledby': 'form-language-menu-button', dense: true }}
// TODO: what to do on resize while menu open?
anchorEl={buttonRef}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
open={isOpen()}
onClose={closeMenu}
PaperProps={{
sx: {
minWidth: '20ch',
},
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<For each={context.localizations}>
{(localization) => {
const isSelected = () => localization === selected();

return (
<MenuItem
dense={true}
selected={isSelected()}
onClick={() => {
setSelected(localization);
closeMenu();
}}
>
<Show when={isSelected()}>
<ListItemIcon>
<Check fontSize="small" />
</ListItemIcon>
</Show>
<div>
<PageMenuButton
id="form-language-menu-button"
ref={buttonRef!}
aria-controls={isOpen() ? 'form-language-menu' : ''}
aria-expanded={isOpen()}
aria-aria-haspopup={true}
onClick={() => {
setIsOpen((current) => !current);
}}
variant="contained"
>
<Stack alignItems="center" direction="row">
<FormLanguageMenuButtonIcon fontSize="small" />
<span style={{ 'line-height': 1 }}>{props.translations.getActiveLanguage()}</span>
<FormLanguageMenuExpandMoreIcon fontSize="small" />
</Stack>
</PageMenuButton>
<Menu
id="form-language-menu"
MenuListProps={{ 'aria-labelledby': 'form-language-menu-button', dense: true }}
// TODO: what to do on resize while menu open?
anchorEl={buttonRef!}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
open={isOpen()}
onClose={closeMenu}
PaperProps={{
sx: {
minWidth: '20ch',
},
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
>
<For each={props.translations.getLanguages()}>
{(language) => {
const isSelected = () => {
return language === props.translations.getActiveLanguage();
};

<ListItemText inset={!isSelected()} disableTypography={true}>
<MenuItemSmallTypography>{localization.name}</MenuItemSmallTypography>
</ListItemText>
</MenuItem>
);
return (
<MenuItem
class="form-language"
dense={true}
selected={isSelected()}
onClick={() => {
props.translations.setActiveLanguage(language);
closeMenu();
}}
</For>
</Menu>
</div>
);
}}
</Show>
>
<Show when={isSelected()}>
<ListItemIcon>
<Check fontSize="small" />
</ListItemIcon>
</Show>

<ListItemText inset={!isSelected()} disableTypography={true}>
<MenuItemSmallTypography>{language}</MenuItemSmallTypography>
</ListItemText>
</MenuItem>
);
}}
</For>
</Menu>
</div>
);
};
Empty file.
4 changes: 3 additions & 1 deletion packages/odk-web-forms/src/components/Page/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { JSX } from 'solid-js';
import { GlobalStyles, Stack, useTheme } from 'suid/material';
import type { EntryState } from '../../lib/xform/state/EntryState.ts';
import { PageContainer } from '../styled/PageContainer.tsx';
import { PageFooter } from './PageFooter.tsx';
import { PageHeader } from './PageHeader.tsx';
import { PageMain } from './PageMain.tsx';

interface PageProps {
readonly children?: JSX.Element;
readonly entry: EntryState | null;
}

export const Page = (props: PageProps) => {
Expand Down Expand Up @@ -36,7 +38,7 @@ export const Page = (props: PageProps) => {
/>
<PageContainer>
<Stack spacing={2}>
<PageHeader />
<PageHeader entry={props.entry} />
<PageMain elevation={2}>{props.children}</PageMain>
<PageFooter />
</Stack>
Expand Down
Loading
Loading