Skip to content
Open
Show file tree
Hide file tree
Changes from 14 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
103 changes: 103 additions & 0 deletions packages/i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<!--
Copyright 2021-Present The Serverless Workflow Specification Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

# i18n Usage Guide

This guide explains how to use the `@serverlessworkflow/i18n` package inside your project (e.g., in the `DiagramEditor`).

---

## What this package provides

- `I18nProvider` → React context provider for translations
- `useI18n()` → Hook to access translations
- `detectLocale()` → Automatically detect user language
- `createI18n()` → Core translation logic (used internally)

---

## Step 1: Define your dictionaries

Create a file like:

```ts
// i18n/locales.ts

export const dictionaries = {
en: {
save: "Save",
},
fr: {
save: "Enregistrer",
},
};
```

- Keys (`save`) must be consistent across languages.

---

## Step 2: Detect or pass locale

You can either:

- Pass `locale` manually via props
- Or auto-detect using `detectLocale`

Example:

```ts
const supportedLocales = Object.keys(dictionaries);

const locale = props.locale ?? detectLocale(supportedLocales);
```

---

## Step 3: Wrap your app with `I18nProvider`

```tsx
import { I18nProvider } from "@serverlessworkflow/i18n";
import { dictionaries } from "../i18n/locales";

<I18nProvider locale={locale} dictionaries={dictionaries}>
{/* your app */}
</I18nProvider>;
```

---

## Step 4: Use translations with `useI18n`

Inside any child component:

```tsx
import { useI18n } from "@serverlessworkflow/i18n";

const Content = () => {
const { t } = useI18n();

return <p>{t("save")}</p>;
};
```

- If the key is missing, it will return the key itself:

```ts
t("unknown") → "unknown"
```

---
43 changes: 43 additions & 0 deletions packages/i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@serverlessworkflow/i18n",
"version": "1.0.0",
"files": [
"dist",
"src"
],
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"clean": "rimraf ./dist",
"build:dev": "pnpm clean && tsc -p tsconfig.json",
"build:prod": "pnpm run build",
"test": "vitest run --passWithNoTests"
},
"devDependencies": {
"@storybook/addon-vitest": "catalog:",
"@storybook/react-vite": "catalog:",
"@testing-library/jest-dom": "catalog:",
"@testing-library/react": "catalog:",
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"rimraf": "catalog:",
"typescript": "catalog:",
"vite": "catalog:",
"vite-tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
}
}
29 changes: 29 additions & 0 deletions packages/i18n/src/core/createI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export type Dictionary = Record<string, string>;
export type Dictionaries = Record<string, Dictionary>;

export function createI18n(dictionaries: Dictionaries, locale: string) {
function t(key: string): string {
return dictionaries[locale]?.[key] ?? key;
}

return {
t,
locale,
};
}
19 changes: 19 additions & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export * from "./react/I18nProvider";
export * from "./core/createI18n";
export * from "./utils/detectLocale";
46 changes: 46 additions & 0 deletions packages/i18n/src/react/I18nProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import React, { createContext, useContext, useMemo } from "react";
import { createI18n, Dictionaries } from "../core/createI18n";

type I18nContextType = ReturnType<typeof createI18n>;

const I18nContext = createContext<I18nContextType | null>(null);

export const I18nProvider = ({
children,
locale,
dictionaries,
}: {
children: React.ReactNode;
locale: string;
dictionaries: Dictionaries;
}) => {
const i18n = useMemo(() => {
return createI18n(dictionaries, locale);
}, [locale, dictionaries]);

return <I18nContext.Provider value={i18n}>{children}</I18nContext.Provider>;
};

export const useI18n = () => {
const ctx = useContext(I18nContext);
if (!ctx) {
throw new Error("useI18n must be used inside I18nProvider");
}
return ctx;
};
36 changes: 36 additions & 0 deletions packages/i18n/src/utils/detectLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

export function detectLocale(supportedLocales: readonly string[], fallback: string = "en"): string {
if (typeof navigator === "undefined") {
return fallback;
}
const languages =
navigator.languages && navigator.languages.length > 0
? navigator.languages
: [navigator.language];

const normalizedSupported = supportedLocales.map((l) => l.toLowerCase().trim());
for (const lang of languages) {
if (!lang) continue;

const short = lang.split("-")[0]?.toLowerCase().trim();
if (short && normalizedSupported.includes(short)) {
return short;
}
}
return fallback;
}
40 changes: 40 additions & 0 deletions packages/i18n/tests/I18nProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { I18nProvider, useI18n } from "../src/react/I18nProvider";

const dictionaries = {
en: { save: "Save" },
};

const TestComponent = () => {
const { t } = useI18n();
return <span>{t("save")}</span>;
};

describe("I18nProvider", () => {
it("provides translation", () => {
render(
<I18nProvider locale="en" dictionaries={dictionaries}>
<TestComponent />
</I18nProvider>,
);

expect(screen.getByText("Save")).toBeInTheDocument();
});
});
35 changes: 35 additions & 0 deletions packages/i18n/tests/createI18n.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, it, expect } from "vitest";
import { createI18n } from "../src/core/createI18n";

describe("createI18n", () => {
const dictionaries = {
en: { save: "Save" },
fr: { save: "Enregistrer" },
};

it("returns correct translation", () => {
const i18n = createI18n(dictionaries, "en");
expect(i18n.t("save")).toBe("Save");
});

it("returns key if missing", () => {
const i18n = createI18n(dictionaries, "fr");
expect(i18n.t("cancel")).toBe("cancel");
});
});
38 changes: 38 additions & 0 deletions packages/i18n/tests/detectLocale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { describe, it, expect, vi } from "vitest";
import { detectLocale } from "../src/utils/detectLocale";

describe("detectLocale", () => {
it("detects supported language", () => {
vi.stubGlobal("navigator", {
languages: ["fr-FR"],
});

const result = detectLocale(["en", "fr"]);
expect(result).toBe("fr");
});

it("falls back if no match", () => {
vi.stubGlobal("navigator", {
languages: ["de-DE"],
});

const result = detectLocale(["en", "fr"]);
expect(result).toBe("en");
});
});
Loading
Loading