Skip to content

Commit

Permalink
Add service intl example
Browse files Browse the repository at this point in the history
  • Loading branch information
mbeckem committed Jun 28, 2024
1 parent c409926 commit 3374545
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 75 deletions.
6 changes: 4 additions & 2 deletions docs/reference/Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ See documentation and examples in [Package Reference](./Package.md) for more det

## Service Options

The service constructor accepts a single object (`serviceOptions` in the example below).
This object is generated by the framework:
All services receive a `serviceOptions` parameter in their constructor.
This parameter is an object generated by the `@open-pioneer/runtime` package that provides access to the rest of the system.

The `serviceOptions` parameter conforms to the interface `ServiceOptions` exported by `@open-pioneer/runtime` (see [TypeScript integration](#typescript-integration)).

```js
// MyService.js
Expand Down
158 changes: 124 additions & 34 deletions docs/tutorials/HowToTranslateAnApp.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,20 @@ import { createCustomElement } from "@open-pioneer/runtime";
import * as appMetadata from "open-pioneer:app";
import { AppUI } from "./AppUI";
// Reads the 'lang' parameter from the URL and, if set, uses it
// for the application's locale.
// This can be helpful during development, but its entirely optional.
const URL_PARAMS = new URLSearchParams(window.location.search);
const FORCED_LANG = URL_PARAMS.get("lang") || undefined;
const Element = createCustomElement({
component: AppUI,
appMetadata,
async resolveConfig(ctx) {
const locale = ctx.getAttribute("forced-locale");
if (!locale) {
return undefined;
}
return { locale };
config: {
// Forces the locale if set to a string.
// 'undefined' choses an automatic locale based on the app and
// the user's preferred languages.
locale: FORCED_LANG
}
});
customElements.define("i18n-howto", Element);
Expand All @@ -78,23 +83,8 @@ and the `index.html`:
<title>Empty Site</title>
</head>
<body>
<div id="container"></div>
<script type="module">
import "./empty/app.ts";
const container = document.getElementById("container");
initApp();
function initApp() {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const lang = urlParams.get("lang");
const app = document.createElement("i18n-howto");
if (lang) {
app.setAttribute("forced-locale", lang);
}
container.appendChild(app);
}
</script>
<i18n-howto></i18n-howto>
<script type="module" src="./app/app.ts"></script>
</body>
</html>
```
Expand All @@ -110,7 +100,7 @@ Now we are able to force the locale with `lang` parameter:

In a first step we prepare our AppUI:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
export function AppUI() {
const intl = useIntl();
Expand All @@ -133,7 +123,7 @@ In addition, we define the `ExampleStack` as a container for our advanced exampl

First we generate an entry in our `ExampleStack` for our `InterpolationExample`:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function ExampleStack() {
return (
Expand All @@ -154,7 +144,7 @@ function ExampleStack() {

Interpolation allows replacement with dynamic values. That's why we use a text input field in our interpolation example:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function InterpolationExample() {
const intl = useIntl();
Expand Down Expand Up @@ -203,7 +193,7 @@ In the rendered text the passed value of `name` will replace the placeholder.

We generate another entry in our `ExampleStack` for our `PluralsExample`:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function ExampleStack() {
return (
Expand All @@ -228,7 +218,7 @@ function ExampleStack() {
With plural support we can output different text depending on a count value (see [Link](https://formatjs.io/docs/core-concepts/icu-syntax/#plural-format)).
We will use a RadioGroup to change the count value in our example:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function PluralsExample() {
const intl = useIntl();
Expand Down Expand Up @@ -282,7 +272,7 @@ The `plurals.value` key defines a count parameter `n`. In result the passed valu

Let's add an entry for `SelectionExample` in our `ExampleStack`:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function ExampleStack() {
return (
Expand Down Expand Up @@ -311,7 +301,7 @@ With selection support we can output different text depending on a set of given
In our example we will change the title depending on a gender selection.
We will use a text input for name and a RadioGroup for gender selection:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function SelectionExample() {
const intl = useIntl();
Expand Down Expand Up @@ -379,7 +369,7 @@ In result the passed value of `gender` and `name` will be used to generate the m

Now we add an entry for `NumberFormatExample` to our `ExampleStack` :

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function ExampleStack() {
return (
Expand Down Expand Up @@ -410,7 +400,7 @@ function ExampleStack() {
With `formatNumber` we can not only format numbers locale specific, but also use units and currencies (see [Link](https://formatjs.io/docs/react-intl/api#formatnumber)).
In our example we will have a number input and an output with different forms of unit and currency:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function NumberFormatExample() {
const intl = useIntl();
Expand Down Expand Up @@ -487,7 +477,7 @@ We pass the `value` with different `NumberFormatOptions` to `intl.formatNumber`.

Finally, we add an entry for the `DateTimeFormatExample` to our `ExampleStack` :

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function ExampleStack() {
return (
Expand Down Expand Up @@ -520,7 +510,7 @@ function ExampleStack() {

In our example we will have a date time input:

```javascript
```tsx
// src/apps/empty/AppUI.tsx
function DateTimeFormatExample() {
const intl = useIntl();
Expand Down Expand Up @@ -588,6 +578,106 @@ In result, we see our selected formatted datetime and the relative time between
> It always uses the defined browser locale or the system default. In our example, if your browser uses locale `de`
> but your app uses url parameter `lang=en` the input will show values matching to locale `de`.

### Using i18n in a service

The `ServiceI18nExample` component displays a plain and simple react form using the `intl` API you have already seen.
In addition to using i18n from within the React component, it also interacts with a service (`GreetingService`) to show a translated message.
The form simply asks the user for a name and then calls the `greetingService.greet(name)` method to show a greeting:

```tsx
// src/apps/empty/AppUI.tsx
function ServiceI18nExample() {
const intl = useIntl();
const greetingService = useService<GreetingService>("i18n-howto-app.GreetingService");
const [inputValue, setInputValue] = useState("");
const [greeting, setGreeting] = useState("");
return (
<>
<Heading as="h4" size="md">
{intl.formatMessage({ id: "serviceI18n.heading" })}
</Heading>
<HStack
as="form"
onSubmit={(e) => {
e.preventDefault();
const name = inputValue.trim();
if (name) {
setGreeting(greetingService.greet(name));
} else {
setGreeting("");
}
}}
>
<Input
placeholder={intl.formatMessage({ id: "serviceI18n.placeholder" })}
value={inputValue}
onChange={(evt) => setInputValue(evt.target.value)}
size="md"
/>
<Button type="submit" flexShrink={0}>
{intl.formatMessage({ id: "serviceI18n.showGreeting" })}
</Button>
</HStack>
{greeting && (
<Text>
{intl.formatMessage({ id: "serviceI18n.serviceResponse" })} {greeting}
</Text>
)}
</>
);
}
```

The `GreetingService` uses the `serviceOptions` parameter (provided by the framework) to access the package's `intl` object.
This object can be used in the same way as the `intl` object returned by `useIntl`:

```ts
// src/apps/empty/GreetingService.ts
import { type DECLARE_SERVICE_INTERFACE, ServiceOptions, PackageIntl } from "@open-pioneer/runtime";
export class GreetingService {
declare [DECLARE_SERVICE_INTERFACE]: "i18n-howto-app.GreetingService";
private _intl: PackageIntl;
constructor(serviceOptions: ServiceOptions) {
this._intl = serviceOptions.intl;
}
/**
* Greets the user in the current locale.
*/
greet(name: string): string {
return this._intl.formatMessage({ id: "greetingService.greeting" }, { name });
}
}
```

To make the service usable from the UI, we have to perform some necessary plumbing:

```ts
// src/apps/empty/services.ts
export { GreetingService } from "./GreetingService";
```

```js
// src/apps/empty/build.config.mjs
import { defineBuildConfig } from "@open-pioneer/build-support";
export default defineBuildConfig({
i18n: ["de", "en"],
services: {
GreetingService: {
provides: ["i18n-howto-app.GreetingService"]
}
},
ui: {
references: ["i18n-howto-app.GreetingService"]
}
});
```

## Demo App

The complete app `i18n-howto` can be found in the `samples` folder.
Expand Down
76 changes: 62 additions & 14 deletions src/samples/i18n-howto/app/AppUI.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import {
Box,
Button,
Container,
HStack,
Heading,
Text,
Input,
Box,
Stack,
StackDivider,
RadioGroup,
Radio,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
NumberIncrementStepper,
NumberDecrementStepper
Radio,
RadioGroup,
Stack,
StackDivider,
Text
} from "@open-pioneer/chakra-integration";
import { useIntl } from "open-pioneer:react-hooks";
import { useIntl, useService } from "open-pioneer:react-hooks";
import { useState } from "react";
import { GreetingService } from "./GreetingService";

export function AppUI() {
const intl = useIntl();
Expand All @@ -42,19 +45,22 @@ function ExampleStack() {
align="stretch"
>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<InterpolationExample></InterpolationExample>
<InterpolationExample />
</Box>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<PluralsExample></PluralsExample>
<PluralsExample />
</Box>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<SelectionExample></SelectionExample>
<SelectionExample />
</Box>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<NumberFormatExample></NumberFormatExample>
<NumberFormatExample />
</Box>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<DateTimeFormatExample></DateTimeFormatExample>
<DateTimeFormatExample />
</Box>
<Box bg="white" w="100%" p={4} color="black" borderWidth="1px" borderColor="black">
<ServiceI18nExample />
</Box>
</Stack>
);
Expand Down Expand Up @@ -222,6 +228,48 @@ function DateTimeFormatExample() {
);
}

function ServiceI18nExample() {
const intl = useIntl();
const greetingService = useService<GreetingService>("i18n-howto-app.GreetingService");
const [inputValue, setInputValue] = useState("");
const [greeting, setGreeting] = useState("");
return (
<>
<Heading as="h4" size="md">
{intl.formatMessage({ id: "serviceI18n.heading" })}
</Heading>
<HStack
as="form"
onSubmit={(e) => {
e.preventDefault();

const name = inputValue.trim();
if (name) {
setGreeting(greetingService.greet(name));
} else {
setGreeting("");
}
}}
>
<Input
placeholder={intl.formatMessage({ id: "serviceI18n.placeholder" })}
value={inputValue}
onChange={(evt) => setInputValue(evt.target.value)}
size="md"
/>
<Button type="submit" flexShrink={0}>
{intl.formatMessage({ id: "serviceI18n.showGreeting" })}
</Button>
</HStack>
{greeting && (
<Text>
{intl.formatMessage({ id: "serviceI18n.serviceResponse" })} {greeting}
</Text>
)}
</>
);
}

function getDeltaTime(datetime: string): number {
const delta = new Date(datetime).getTime() - new Date().getTime();
return Math.round(delta / 60000);
Expand Down
Loading

0 comments on commit 3374545

Please sign in to comment.