Skip to content

Commit

Permalink
NEW: Translations layer (#1016)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomashapl authored and vepor committed Apr 30, 2019
1 parent 10f17a9 commit bfa7a90
Show file tree
Hide file tree
Showing 48 changed files with 369 additions and 30 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Expand Up @@ -55,6 +55,7 @@
"import/no-self-import": "off",
"react/destructuring-assignment": "off",
"react/no-access-state-in-setstate": "off",
"jsx-a11y/label-has-associated-control": "off"
"jsx-a11y/label-has-associated-control": "off",
"no-await-in-loop": "off"
}
}
43 changes: 43 additions & 0 deletions .github/dictionary.md
@@ -0,0 +1,43 @@
# Dictionary
We added support to our own dictionary. It handles common translations in components like `Close`

You have available our dictionary in `@kiwicom/orbit-components/lib/data/dictionary/...`
There are files which contains our own translations.

**Example:**
```jsx
import en_GB from "@kiwicom/orbit-components/lib/data/dictionary/en-GB.json";
import ThemeProvider from "@kiwicom/orbit-components/lib/ThemeProvider";
import Tooltip from "@kiwicom/orbit-components/lib/Tooltip";
import Heading from "@kiwicom/orbit-components/lib/Heading";

const App = () =>
<ThemeProvider dictionary={en_GB}>
<Tooltip content="Write your text here. If it’s longer than three li…">
<Heading>
Orbit design system
</Heading>
</Tooltip>
</ThemeProvider>;
```

**Fallbacks**

* If translation key not exists in your language the fallback is `en_GB` which is our default lang
* If translation key not exists in both files (your language, default language), the translation key will be rendered e.g. `button_close`

**Your own dictionary**

There is option to add your own dictionary, just pass object containing keys and values.

```jsx
import ThemeProvider from "@kiwicom/orbit-components/lib/ThemeProvider";

const App = () =>
<ThemeProvider dictionary={{
"button_close": "My own translation"
}}>
<Button type="secondary" size="large" />
</ThemeProvider>;
```

6 changes: 3 additions & 3 deletions .github/theming.md
@@ -1,12 +1,12 @@
# Theming
Our entire CSS styling for our components is based on [orbit-design-tokens](https://github.com/kiwicom/orbit-design-tokens), which contains variables with colors, sizings, spacings, etc. It also contains the functionality to create custom themes that can be used inside `orbit-components`.

All you need to do is pass colors into the `getTokens` function and then pass this object into `<ThemeProvider />`. The component from `styled-components` will do all the magic for you thanks to React's context API.
All you need to do is pass colors into the `getTokens` function and then pass this object into `<ThemeProvider />`. The component re-exports `ThemeProvider` from `styled-components` and will do all the magic for you thanks to React's context API.

**Example:**
```jsx
import { getTokens } from "@kiwicom/orbit-design-tokens";
import { ThemeProvider } from "styled-components";
import getTokens from "@kiwicom/orbit-components/lib/getTokens";
import ThemeProvider from "@kiwicom/orbit-components/lib/ThemeProvider";

const customTokens = getTokens({
palette: {
Expand Down
72 changes: 72 additions & 0 deletions config/fetchTranslations.js
@@ -0,0 +1,72 @@
// @flow
import fetch from "isomorphic-unfetch";
import path from "path";
import dotenv from "dotenv";
import fs from "fs-extra";

dotenv.config();
const env = name => process.env[name] || "";

const PHRASE_APP_BASE_URL = "https://api.phraseapp.com/api/v2";
const PHRASE_APP_PROJECT_ID = env("PHRASE_APP_PROJECT_ID");
const PHRASE_APP_ACCESS_TOKEN = env("PHRASE_APP_ACCESS_TOKEN");

const LOCALES_URL = `${PHRASE_APP_BASE_URL}/projects/${PHRASE_APP_PROJECT_ID}/locales`;
const FILE_FORMAT = "nested_json";

const fetchJSON = async url => {
const options = {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `token ${PHRASE_APP_ACCESS_TOKEN}`,
},
};
return (await fetch(url, options)).json();
};

const writeJSON = (filename, obj) =>
new Promise((resolve, reject) => {
fs.outputFile(filename, JSON.stringify(obj, null, 2), "utf8", err => {
if (err) {
reject(err);
}

resolve();
});
});

const flatten = (obj = {}, keyPrefix = "") =>
Object.entries(obj).reduce((result, [key, value]) => {
if (value && typeof value === "object") {
return {
...result,
...flatten(value, `${keyPrefix}${key}.`),
};
}
return {
...result,
[keyPrefix + key]: value,
};
}, {});

(async () => {
try {
const allLocales = await fetchJSON(LOCALES_URL);
const LOCALES_DATA = path.join(__dirname, "..", "src", "data", "dictionary");

// PhraseApp has limits on parallel requests
// that's why we process requests in sequence
// eslint-disable-next-line no-restricted-syntax
for (const locale of allLocales) {
const translation = await fetchJSON(
`${LOCALES_URL}/${locale.id}/download?file_format=${FILE_FORMAT}&tags=orbit&encoding=UTF-8`,
);

await writeJSON(path.join(LOCALES_DATA, `${locale.code}.json`), flatten(translation.orbit));
}
} catch (error) {
console.error(error); // eslint-disable-line no-console
process.exit(1);
}
})();
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -18,6 +18,7 @@
"flow:check": "flow check",
"test": "jest --config=jest.json",
"test-ci": "yarn flow:check && yarn eslint && yarn test --ci --maxWorkers=2",
"fetch-translations": "babel-node config/fetchTranslations.js",
"deploy:storybook": "storybook-to-ghpages",
"deploy:surge": "yarn surge .out/ https://orbit-components-$(git rev-parse --abbrev-ref HEAD | sed -e 's/[^a-zA-Z0-9]/-/g').surge.sh",
"deploy:updateURL": "yarn babel-node config/deploymentUtils.js updateLiveURL"
Expand Down Expand Up @@ -107,6 +108,7 @@
"camelcase": "^5.0.0",
"capitalize": "^1.0.0",
"copyfiles": "^2.1.0",
"dotenv": "^7.0.0",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.6.0",
"enzyme-to-json": "^3.3.3",
Expand All @@ -120,7 +122,9 @@
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.7.0",
"flow-bin": "^0.89.0",
"fs-extra": "^7.0.1",
"glob": "^7.1.2",
"isomorphic-unfetch": "^3.0.0",
"jest": "^24.5.0",
"jest-styled-components": "^6.2.1",
"jsdom": "^12.0.0",
Expand Down
3 changes: 2 additions & 1 deletion readme.md
Expand Up @@ -49,7 +49,7 @@ import Alert from "@kiwicom/orbit-components/lib/Alert"
<Alert>Hello World!</Alert>
```

If you want to use a custom theme inside your project, its necessary to generate a theme object from `orbit-design-tokens` and use it in `<ThemeProvider />` component from `styled-components`. See [this document](https://github.com/kiwicom/orbit-components/tree/master/.github/theming.md) for more information.
If you want to use custom theme or dictionary inside your project, it's necessary to wrap your app into `<ThemeProvider>`. See [this document](https://github.com/kiwicom/orbit-components/tree/master/src/ThemeProvider/README.md) for more information.

For live preview check out [Storybook](https://kiwicom.github.io/orbit-components/) or [orbit.kiwi](https://orbit.kiwi).

Expand All @@ -60,6 +60,7 @@ You can also try `orbit-components` live on [CodeSandbox](https://codesandbox.io
* [Icons](https://github.com/kiwicom/orbit-components/tree/master/src/Icon/README.md)
* [Right to left languages](https://github.com/kiwicom/orbit-components/tree/master/src/utils/rtl/README.md)
* [Theming](https://github.com/kiwicom/orbit-components/tree/master/.github/theming.md)
* [Dictionary](https://github.com/kiwicom/orbit-components/tree/master/.github/dictionary.md)

## Contributing
We are working on making this project fully open source. We appreciate any contributions you might make.
Expand Down
12 changes: 12 additions & 0 deletions src/Dictionary/index.js
@@ -0,0 +1,12 @@
// @flow
import * as React from "react";

import type { DictionaryContextType, Props } from "./index";

export const DictionaryContext: DictionaryContextType = React.createContext({});

const Dictionary = ({ values, children }: Props) => (
<DictionaryContext.Provider value={values}>{children}</DictionaryContext.Provider>
);

export default Dictionary;
17 changes: 17 additions & 0 deletions src/Dictionary/index.js.flow
@@ -0,0 +1,17 @@
// @flow
import * as React from "react";

export type Translations = {
[key: string]: string,
};

export type DictionaryContextType = React.Context<Translations>;

export type Props = {
+values: Translations,
+children: React$Node,
};

declare export default React$ComponentType<Props>;

declare export var DictionaryContext: DictionaryContextType;
1 change: 0 additions & 1 deletion src/Popover/README.MD
Expand Up @@ -16,7 +16,6 @@ Table below contains all types of the props available in the Popover component.
| Name | Type | Default | Description |
| :---------------- | :--------------------- | :-------------- | :------------------------------- |
| **content** | `React.Node` | | The content to display in the Popover.
| closeText | `Translation` | `"Close"` | The text of the close button to display on mobile devices.
| **children** | `React.Node` | | The reference element where the Popover will appear.
| dataTest | `string` | | Optional prop for testing purposes.
| noPadding | `boolean` | `true` | Adds or removes padding around popover's content.
Expand Down
8 changes: 5 additions & 3 deletions src/Popover/__tests__/__snapshots__/index.test.js.snap
Expand Up @@ -921,7 +921,7 @@ exports[`ContentWrapper it should match snapshot 1`] = `
<Button>
Content
</Button>
<ContentWrapper__StyledTooltipClose
<ContentWrapper__StyledPopoverClose
theme={
Object {
"orbit": Object {
Expand Down Expand Up @@ -1378,9 +1378,11 @@ exports[`ContentWrapper it should match snapshot 1`] = `
onClick={[MockFunction]}
type="secondary"
>
Close
<Translate
tKey="button_close"
/>
</Button>
</ContentWrapper__StyledTooltipClose>
</ContentWrapper__StyledPopoverClose>
</ContentWrapper__StyledPopoverContent>
</ContentWrapper__StyledPopoverParent>
</Fragment>
Expand Down
12 changes: 6 additions & 6 deletions src/Popover/components/ContentWrapper.js
Expand Up @@ -12,6 +12,7 @@ import type { Props } from "./ContentWrapper.js.flow";
import useDimensions from "../hooks/useDimensions";
import useVerticalPosition from "../hooks/useVerticalPosition";
import useHorizontalPosition from "../hooks/useHorizontalPosition";
import Translate from "../../Translate";

const showAnimation = keyframes`
from {
Expand Down Expand Up @@ -93,7 +94,7 @@ StyledOverlay.defaultProps = {
theme: defaultTheme,
};

const StyledTooltipClose = styled.div`
const StyledPopoverClose = styled.div`
padding: ${({ theme, noPadding }) => (noPadding ? theme.orbit.spaceSmall : 0)};
padding-top: ${({ theme }) => theme.orbit.spaceMedium};
Expand All @@ -103,13 +104,12 @@ const StyledTooltipClose = styled.div`
padding-bottom: 0;
`)}
`;
StyledTooltipClose.defaultProps = {
StyledPopoverClose.defaultProps = {
theme: defaultTheme,
};

const PopoverContentWrapper = ({
children,
closeText,
onClose,
width,
dataTest,
Expand Down Expand Up @@ -160,11 +160,11 @@ const PopoverContentWrapper = ({
>
<StyledPopoverContent ref={content}>
{children}
<StyledTooltipClose noPadding={noPadding}>
<StyledPopoverClose noPadding={noPadding}>
<Button type="secondary" block onClick={onClose}>
{closeText || "Close"}
<Translate tKey="button_close" />
</Button>
</StyledTooltipClose>
</StyledPopoverClose>
</StyledPopoverContent>
</StyledPopoverParent>
</React.Fragment>
Expand Down
3 changes: 1 addition & 2 deletions src/Popover/index.js.flow
@@ -1,5 +1,5 @@
// @flow
import type { Globals, Translation } from "../common/common.js.flow";
import type { Globals } from "../common/common.js.flow";

export type PositionsCore = "top" | "bottom";
export type AnchorsCore = "start" | "end";
Expand Down Expand Up @@ -28,7 +28,6 @@ export type Props = {|
...Globals,
+children: React$Node,
+content: React$Node,
+closeText?: Translation,
+preferredPosition?: PositionsCore,
+opened?: boolean,
+width?: string,
Expand Down
20 changes: 20 additions & 0 deletions src/ThemeProvider/README.md
@@ -0,0 +1,20 @@
# ThemeProvider
orbit-components has theming support via our own `<ThemeProvider>` which adds you possibilities to add your own theme and dictionary.
```jsx
import ThemeProvider from "@kiwicom/orbit-components/lib/ThemeProvider";
```
After adding import please wrap your application into `ThemeProvider` and you can provide your own [`theme`](https://github.com/kiwicom/orbit-components/blob/master/.github/theming.md) and [`dictionary`](https://github.com/kiwicom/orbit-components/blob/master/.github/dictionary.md)

```jsx
<ThemeProvider theme={...} dictionary={...}>
<App />
</ThemeProvider>
```
## Props
Table below contains all types of the props available in the ThemeProvider component.

| Name | Type | Default | Description |
| :------------ | :------------------------ | :-------------- | :------------------------------- |
| **children** | `React.Node` | | Your app
| theme | `[Object]` | | See [`theming`](https://github.com/kiwicom/orbit-components/blob/master/.github/theming.md)
| dictionary | `[Object]` | | See [`dictionary`](https://github.com/kiwicom/orbit-components/blob/master/.github/dictionary.md)
19 changes: 19 additions & 0 deletions src/ThemeProvider/index.js
@@ -0,0 +1,19 @@
// @flow
import React from "react";
import { ThemeProvider as StyledThemeProvider } from "styled-components";

import Dictionary from "../Dictionary";

import type { Props } from "./index";

const ThemeProvider = ({ theme, dictionary, children }: Props) => (
<StyledThemeProvider theme={theme}>
{dictionary ? (
<Dictionary values={dictionary}>{React.Children.only(children)}</Dictionary>
) : (
React.Children.only(children)
)}
</StyledThemeProvider>
);

export default ThemeProvider;
14 changes: 14 additions & 0 deletions src/ThemeProvider/index.js.flow
@@ -0,0 +1,14 @@
// @flow
/*
DOCUMENTATION: https://orbit.kiwi/components/themeprovider/
*/

import type { Translations } from "../Dictionary";

export type Props = {|
+theme: any,
+dictionary?: Translations,
+children: React$Node,
|};

declare export default React$ComponentType<Props>;
1 change: 0 additions & 1 deletion src/Tooltip/README.md
Expand Up @@ -20,7 +20,6 @@ Table below contains all types of the props available in the Tooltip component.
| **content** | `React.Node` | | The content to display in the Tooltip.
| preferredPosition | [`enum`](#enum) | | The preferred position to choose [See Functional specs](#functional-specs)
| size | [`enum`](#enum) | | The maximum possible size of the Tooltip.
| closeText | `Translation` | | The text of the close button to display on mobile devices.
| tabIndex | `string` | `"0"` | Specifies the tab order of an element

## enum
Expand Down

0 comments on commit bfa7a90

Please sign in to comment.