diff --git a/packages/graphic-walker/README.md b/packages/graphic-walker/README.md index 618edbe1..b7ded9ea 100644 --- a/packages/graphic-walker/README.md +++ b/packages/graphic-walker/README.md @@ -41,3 +41,50 @@ export default YourEmbeddingTableauStyleApp; # packages/graphic-walker npm run dev ``` + + +## I18n Support + +Graphic Walker now support _English_ (as `"en"` or `"en-US"`) and _Chinese_ (as `"zh"` or `"zh-CN"`) with built-in locale resources. You can simply provide a valid string value (enumerated above) as `props.i18nLang` to set a language or synchronize your global i18n language with the component like the example given as follow. + +```typescript +const YourApp = props => { + // ... + + const curLang = /* get your i18n language */; + + return +} +``` + +### Customize I18n + +If you need i18n support to cover languages not supported currently, or to totally rewrite the content of any built-in resource(s), you can also provide your resource(s) as `props.i18nResources` to Graphic Walker like this. + +```typescript +const yourResources = { + 'de-DE': { + ... + }, + 'fr-FE': { + ... + }, +}; + +const YourApp = props => { + // ... + + const curLang = /* get your i18n language */; + + return +} +``` diff --git a/packages/graphic-walker/package.json b/packages/graphic-walker/package.json index 169f6c5f..25873440 100644 --- a/packages/graphic-walker/package.json +++ b/packages/graphic-walker/package.json @@ -2,7 +2,9 @@ "name": "@kanaries/graphic-walker", "version": "0.1.0", "scripts": { - "dev": "vite --host", + "dev:front_end": "vite --host", + "dev:data_service": "npx serve ../rath-client/public -l 8080", + "dev": "concurrently \"npm run dev:front_end\" \"npm run dev:data_service\"", "build": "tsc && vite build", "serve": "vite preview", "type": "tsc src/lib.ts --declaration --emitDeclarationOnly --jsx react --esModuleInterop --outDir dist" @@ -27,6 +29,8 @@ "@heroicons/react": "^2.0.8", "@kanaries/web-data-loader": "0.1.5", "autoprefixer": "^10.3.5", + "i18next": "^21.9.1", + "i18next-browser-languagedetector": "^6.1.5", "mobx": "^6.3.3", "mobx-react-lite": "^3.2.1", "postcss": "^8.3.7", @@ -34,6 +38,7 @@ "react": "^17.0.2", "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", + "react-i18next": "^11.18.6", "react-json-view": "^1.21.3", "rxjs": "^7.3.0", "styled-components": "^5.3.0", diff --git a/packages/graphic-walker/src/App.tsx b/packages/graphic-walker/src/App.tsx index 89f95809..5caef497 100644 --- a/packages/graphic-walker/src/App.tsx +++ b/packages/graphic-walker/src/App.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Record, IMutField } from './interfaces'; +import { IMutField, IRow } from './interfaces'; import VisualSettings from './visualSettings'; import { Container, NestContainer } from './components/container'; import ClickMenu from './components/clickMenu'; @@ -17,22 +17,42 @@ import { toJS } from 'mobx'; import "tailwindcss/tailwind.css" import './index.css' import { Specification } from 'visual-insights'; -import PureTabs from './components/tabs/pureTab'; +// import PureTabs from './components/tabs/pureTab'; import VisNav from './segments/visNav'; +import { useTranslation } from 'react-i18next'; +import { mergeLocaleRes, setLocaleLanguage } from './locales/i18n'; + export interface EditorProps { - dataSource?: Record[]; + dataSource?: IRow[]; rawFields?: IMutField[]; - spec?: Specification + spec?: Specification; + i18nLang?: string; + i18nResources?: { [lang: string]: Record }; } const App: React.FC = props => { - const { dataSource = [], rawFields = [], spec } = props; + const { dataSource = [], rawFields = [], spec, i18nLang = 'en-US', i18nResources } = props; const { commonStore, vizStore } = useGlobalStore(); const [insightReady, setInsightReady] = useState(true); const { currentDataset, datasets, vizEmbededMenu } = commonStore; + const { t, i18n } = useTranslation(); + const curLang = i18n.language; + + useEffect(() => { + if (i18nResources) { + mergeLocaleRes(i18nResources); + } + }, [i18nResources]); + + useEffect(() => { + if (i18nLang !== curLang) { + setLocaleLanguage(i18nLang); + } + }, [i18nLang, curLang]); + // use as an embeding module, use outside datasource from props. useEffect(() => { if (dataSource.length > 0) { @@ -75,7 +95,7 @@ const App: React.FC = props => { {/* {}} /> */} - +
@@ -94,13 +114,16 @@ const App: React.FC = props => { {vizEmbededMenu.show && ( -
{ commonStore.closeEmbededMenu(); commonStore.setShowInsightBoard(true) }} > - 数据解读 + + {t('App.labels.data_interpretation')} + +
)} diff --git a/packages/graphic-walker/src/components/clickMenu.tsx b/packages/graphic-walker/src/components/clickMenu.tsx index e078a66e..2204d353 100644 --- a/packages/graphic-walker/src/components/clickMenu.tsx +++ b/packages/graphic-walker/src/components/clickMenu.tsx @@ -2,7 +2,7 @@ import React from 'react'; import styled from 'styled-components'; const MenuContainer = styled.div` - width: 100px; + min-width: 100px; background-color: #fff; border: 1px solid #f0f0f0; position: absolute; diff --git a/packages/graphic-walker/src/components/container.tsx b/packages/graphic-walker/src/components/container.tsx index 48527518..5f554eff 100644 --- a/packages/graphic-walker/src/components/container.tsx +++ b/packages/graphic-walker/src/components/container.tsx @@ -10,6 +10,7 @@ export const Container = styled.div` export const NestContainer = styled.div` border: 1px solid #d9d9d9; padding: 0.4em; + font-size: 12px; margin: 0.2em; background-color: #fff; ` \ No newline at end of file diff --git a/packages/graphic-walker/src/components/modal.tsx b/packages/graphic-walker/src/components/modal.tsx index 760de792..ad9b0c6e 100644 --- a/packages/graphic-walker/src/components/modal.tsx +++ b/packages/graphic-walker/src/components/modal.tsx @@ -1,6 +1,18 @@ import React from 'react'; import styled from 'styled-components'; import { XCircleIcon } from '@heroicons/react/24/outline'; + + +const Background = styled.div({ + position: 'fixed', + left: 0, + top: 0, + width: '100vw', + height: '100vh', + backdropFilter: 'blur(2px)', + zIndex: 25535, +}); + const Container = styled.div` width: 880px; max-height: 800px; @@ -25,23 +37,35 @@ const Container = styled.div` z-index: 999; `; interface ModalProps { - onClose?: () => void - title?: string; + onClose?: () => void + title?: string; } const Modal: React.FC = props => { - const { onClose, title } = props; - return ( - -
- {title} - -
-
{props.children}
-
- ) + const { onClose, title } = props; + + return ( + + e.stopPropagation()} + > +
+
+ {title} +
+ +
+
{props.children}
+
+
+ ); } export default Modal; diff --git a/packages/graphic-walker/src/components/sizeSetting.tsx b/packages/graphic-walker/src/components/sizeSetting.tsx index 46fcdb9f..410bd4a0 100644 --- a/packages/graphic-walker/src/components/sizeSetting.tsx +++ b/packages/graphic-walker/src/components/sizeSetting.tsx @@ -1,6 +1,7 @@ import { ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline"; -import React from "react"; -import { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { useTranslation } from 'react-i18next'; + interface SizeSettingProps { onWidthChange: (val: number) => void; @@ -8,31 +9,56 @@ interface SizeSettingProps { width: number; height: number; } + const SizeSetting: React.FC = props => { const { onWidthChange, onHeightChange, width, height } = props const [show, setShow] = useState(false); + const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings.size_setting' }); + + useEffect(() => { + if (show) { + const closeDialog = () => { + setShow(false); + }; + + document.body.addEventListener('click', closeDialog); + + return () => { + document.body.removeEventListener('click', closeDialog); + }; + } + }, [show]); return
{ setShow(v => !v) }} className="w-4 h-4 inline-block mr-0.5 text-gray-900" /> { - show &&
+ show &&
e.stopPropagation()} style={{ zIndex: 25535 }}>
{ - setShow(false); - e.stopPropagation(); - }} - /> + className="text-gray-900 absolute right-2 top-2 w-4 cursor-pointer hover:bg-red-100" + role="button" + tabIndex={0} + aria-label="close" + onClick={(e) => { + setShow(false); + e.stopPropagation(); + }} + />
-
+
= props => { onWidthChange(Math.round(Number(e.target.value) ** 2 * 1000)) }} /> - + + {`${t('width')}: ${width}`} +
= props => { onHeightChange(Math.round(Number(e.target.value) ** 2 * 1000)) }} /> - + + {`${t('height')}: ${height}`} +
diff --git a/packages/graphic-walker/src/components/tabs/pureTab.tsx b/packages/graphic-walker/src/components/tabs/pureTab.tsx index 6f323778..7cd8606b 100644 --- a/packages/graphic-walker/src/components/tabs/pureTab.tsx +++ b/packages/graphic-walker/src/components/tabs/pureTab.tsx @@ -1,4 +1,6 @@ import React, { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + function classNames(...classes: string[]) { return classes.filter(Boolean).join(' ') @@ -7,6 +9,7 @@ function classNames(...classes: string[]) { export interface ITabOption { label: string; key: string; + options?: Record; } interface PureTabsProps { tabs: ITabOption[]; @@ -18,18 +21,26 @@ interface PureTabsProps { export default function PureTabs(props: PureTabsProps) { const { tabs, selectedKey, onSelected, allowEdit, onEditLabel } = props; const [editList, setEditList] = useState([]); + const { t } = useTranslation(); + const clearEditStatus = useCallback(() => { setEditList(new Array(tabs.length).fill(false)) - }, [tabs.length]) + }, [tabs.length]); + useEffect(() => { clearEditStatus - }, [clearEditStatus]) + }, [clearEditStatus]); + return ( -
-