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)) }} /> - 宽度{width} + + {`${t('width')}: ${width}`} + = props => { onHeightChange(Math.round(Number(e.target.value) ** 2 * 1000)) }} /> - 高度{height} + + {`${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 ( - - + + {tabs.map((tab, tabIndex) => ( { onSelected(tab.key, tabIndex) }} @@ -49,7 +60,7 @@ export default function PureTabs(props: PureTabsProps) { tab.key === selectedKey ? "border-transparent text-black bg-gray-100" : "text-gray-500 hover:text-gray-700 hover:border-gray-300", - "whitespace-nowrap border-gray-300 py-1 px-2 border-t border-r botder-b text-sm cursor-pointer" + "whitespace-nowrap border-gray-300 py-1 px-2 border-t border-r border-b pr-6 text-sm cursor-pointer" )} /> ))} diff --git a/packages/graphic-walker/src/config.ts b/packages/graphic-walker/src/config.ts index bc83df65..9f60b645 100644 --- a/packages/graphic-walker/src/config.ts +++ b/packages/graphic-walker/src/config.ts @@ -1,56 +1,20 @@ -export const GEMO_TYPES = [ - { - value: 'auto', - label: '自动' - }, - { - value: 'bar', - label: '条形图', - }, - { - value: 'line', - label: '线', - }, - { - value: 'area', - label: '面', - }, - { - value: 'point', - label: '点', - }, - { - value: 'circle', - label: '圆' - }, - { - value: 'tick', - label: '标记', - }, - { - value: 'rect', - label: '矩形', - }, - { - value: 'arc', - label: '弧形' - }, - { - value: 'boxplot', - label: '统计箱' - }, -]; +export const GEMO_TYPES: Readonly = [ + 'auto', + 'bar', + 'line', + 'area', + 'point', + 'circle', + 'tick', + 'rect', + 'arc', + 'boxplot', +] as const; -export const CHART_LAYOUT_TYPE = [ - { - value: 'auto', - label: '自动' - }, - { - value: 'fixed', - label: '固定' - } -] +export const CHART_LAYOUT_TYPE: Readonly = [ + 'auto', + 'fixed', +] as const; export const COLORS = { // tableau style diff --git a/packages/graphic-walker/src/dataSource/config.ts b/packages/graphic-walker/src/dataSource/config.ts index d78381d4..41c23070 100644 --- a/packages/graphic-walker/src/dataSource/config.ts +++ b/packages/graphic-walker/src/dataSource/config.ts @@ -9,15 +9,15 @@ export const DemoDataAssets = process.env.NODE_ENV === 'production' ? { KELPER: 'https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-kelper-service.json', } : { // CARS: "https://chspace.oss-cn-hongkong.aliyuncs.com/api/ds-cars-service.json", - CARS: "http://localhost:8080/datasets/ds-cars-service.json", + CARS: "/datasets/ds-cars-service.json", // STUDENTS: "https://chspace.oss-cn-hongkong.aliyuncs.com/datasets/ds-students-service.json", - STUDENTS: "http://localhost:8080/datasets/ds-students-service.json", - BTC_GOLD: "http://localhost:8080/datasets/ds_btc_gold_service.json", - BIKE_SHARING: 'http://localhost:8080/datasets/ds-bikesharing-service.json', - CAR_SALES: 'http://localhost:8080/datasets/ds-carsales-service.json', - COLLAGE: 'http://localhost:8080/datasets/ds-collage-service.json', - TITANIC: 'http://localhost:8080/datasets/ds-titanic-service.json', - KELPER: 'http://localhost:8080/datasets/ds-kelper-service.json', + STUDENTS: "/datasets/ds-students-service.json", + BTC_GOLD: "/datasets/ds_btc_gold_service.json", + BIKE_SHARING: '/datasets/ds-bikesharing-service.json', + CAR_SALES: '/datasets/ds-carsales-service.json', + COLLAGE: '/datasets/ds-collage-service.json', + TITANIC: '/datasets/ds-titanic-service.json', + KELPER: '/datasets/ds-kelper-service.json', } as const; interface IPublicData { diff --git a/packages/graphic-walker/src/dataSource/dataSelection/csvData.tsx b/packages/graphic-walker/src/dataSource/dataSelection/csvData.tsx index 16283aa5..43ed99a3 100644 --- a/packages/graphic-walker/src/dataSource/dataSelection/csvData.tsx +++ b/packages/graphic-walker/src/dataSource/dataSelection/csvData.tsx @@ -5,6 +5,7 @@ import Table from '../table'; import styled from 'styled-components'; import { useGlobalStore } from '../../store'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; const Container = styled.div` overflow-x: auto; @@ -19,7 +20,10 @@ const CSVData: React.FC = props => { const onSubmitData = useCallback(() => { commonStore.commitTempDS(); - }, []) + }, []); + + const { t } = useTranslation('translation', { keyPrefix: 'DataSource.dialog.file' }); + return ( = props => { { if (fileRef.current) { fileRef.current.click(); }}} > - 上传数据 + {t('open')} { onSubmitData(); }} > - 确认 + {t('submit')} - 数据集名称 - + {t('dataset_name')} + + { commonStore.updateTempName(e.target.value) diff --git a/packages/graphic-walker/src/dataSource/dataSelection/index.tsx b/packages/graphic-walker/src/dataSource/dataSelection/index.tsx index 125abd07..854d967a 100644 --- a/packages/graphic-walker/src/dataSource/dataSelection/index.tsx +++ b/packages/graphic-walker/src/dataSource/dataSelection/index.tsx @@ -2,30 +2,47 @@ import React from 'react'; import { useState } from 'react'; import CSVData from './csvData'; import PublicData from './publicData'; +import { useTranslation } from 'react-i18next'; interface IDataSelectionProps { } const DataSelection: React.FC = props =>{ - const [sourceType, setSourceType] = useState<'file' | 'public'>('file') + const [sourceType, setSourceType] = useState<'file' | 'public'>('file'); + const { t } = useTranslation('translation', { keyPrefix: 'DataSource' }); + return - Data Types + + {t('dialog.data_types')} + - { setSourceType('file'); }} > - Text File Data + {t('dialog.text_file_data')} - { setSourceType('public'); }} > - Public Data + {t('dialog.public_data')} - Data Source Type [{sourceType}] + + {t('dialog.data_source_type', { sourceType })} + { sourceType === 'file' && diff --git a/packages/graphic-walker/src/dataSource/dataSelection/publicData.tsx b/packages/graphic-walker/src/dataSource/dataSelection/publicData.tsx index af4d17c6..0863d5ae 100644 --- a/packages/graphic-walker/src/dataSource/dataSelection/publicData.tsx +++ b/packages/graphic-walker/src/dataSource/dataSelection/publicData.tsx @@ -3,6 +3,9 @@ import Table from '../table'; import { DemoDataAssets, PUBLIC_DATA_LIST } from '../config' import { useGlobalStore } from '../../store'; import { observer } from 'mobx-react-lite'; +import { useTranslation } from 'react-i18next'; + + interface IPublicDataProps { } @@ -10,6 +13,8 @@ interface IPublicDataProps { const PublicData: React.FC = props => { const { commonStore } = useGlobalStore(); const { tmpDataSource } = commonStore; + const { t } = useTranslation('translation', { keyPrefix: 'DataSource.dialog.public' }); + return { @@ -41,7 +46,9 @@ const PublicData: React.FC = props => { { commonStore.commitTempDS() }} - >确认使用 + > + {t('submit')} + diff --git a/packages/graphic-walker/src/dataSource/index.tsx b/packages/graphic-walker/src/dataSource/index.tsx index 5b2050de..0df09eb5 100644 --- a/packages/graphic-walker/src/dataSource/index.tsx +++ b/packages/graphic-walker/src/dataSource/index.tsx @@ -6,6 +6,7 @@ import Modal from '../components/modal'; import DataSelection from './dataSelection'; import { useGlobalStore } from '../store'; import { CheckCircleIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; +import { useTranslation } from 'react-i18next'; interface DSSegmentProps { preWorkDone: boolean; @@ -14,12 +15,15 @@ interface DSSegmentProps { const DataSourceSegment: React.FC = props => { const { preWorkDone } = props; const { commonStore } = useGlobalStore(); + const { t } = useTranslation(); const { currentDataset, datasets, showDSPanel } = commonStore; return {!preWorkDone && } - 当前数据集 + + {t('DataSource.labels.cur_dataset')} + = props => { { commonStore.startDSBuildingTask() }} - >创建数据集 + > + {t('DataSource.buttons.create_dataset')} + {showDSPanel && ( { commonStore.setShowDSPanel(false) }} > diff --git a/packages/graphic-walker/src/fields/aestheticFields.tsx b/packages/graphic-walker/src/fields/aestheticFields.tsx index 880f5852..1916a219 100644 --- a/packages/graphic-walker/src/fields/aestheticFields.tsx +++ b/packages/graphic-walker/src/fields/aestheticFields.tsx @@ -12,7 +12,7 @@ const aestheticFields = DRAGGABLE_STATE_KEYS.filter(f => ['color', 'opacity', 's const AestheticFields: React.FC = props => { return { - aestheticFields.map(dkey => + aestheticFields.map(dkey => {(provided, snapshot) => ( diff --git a/packages/graphic-walker/src/fields/components.tsx b/packages/graphic-walker/src/fields/components.tsx index 4c116eb0..60e4e907 100644 --- a/packages/graphic-walker/src/fields/components.tsx +++ b/packages/graphic-walker/src/fields/components.tsx @@ -1,6 +1,8 @@ import React from "react"; import styled from "styled-components"; import { COLORS } from "../config"; +import { useTranslation } from 'react-i18next'; + export const AestheticSegment = styled.div` border: 1px solid #dfe3e8; @@ -21,10 +23,12 @@ export const AestheticSegment = styled.div` ` export const FieldListContainer: React.FC<{ name: string }> = (props) => { + const { t } = useTranslation('translation', { keyPrefix: 'constant.draggable_key' }); + return ( - {props.name} + {t(props.name)} {props.children} @@ -32,10 +36,12 @@ export const FieldListContainer: React.FC<{ name: string }> = (props) => { }; export const AestheticFieldContainer: React.FC<{ name: string }> = props => { + const { t } = useTranslation('translation', { keyPrefix: 'constant.draggable_key' }); + return ( - - {props.name} + + {t(props.name)} {props.children} diff --git a/packages/graphic-walker/src/fields/datasetFields/index.tsx b/packages/graphic-walker/src/fields/datasetFields/index.tsx index 682e7cc5..a4f8ff07 100644 --- a/packages/graphic-walker/src/fields/datasetFields/index.tsx +++ b/packages/graphic-walker/src/fields/datasetFields/index.tsx @@ -5,6 +5,7 @@ import { Droppable, Draggable, } from "react-beautiful-dnd"; +import { useTranslation } from 'react-i18next'; import { useGlobalStore } from '../../store'; import DataTypeIcon from '../../components/dataTypeIcon'; @@ -15,6 +16,8 @@ import MeaFields from './meaFields'; const FIELDS_KEY: keyof DraggableFieldState = 'fields'; const DatasetFields: React.FC = props => { + const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.DatasetFields' }); + const { vizStore } = useGlobalStore(); const { draggableFieldState } = vizStore; const { fields } = draggableFieldState; @@ -34,8 +37,10 @@ const DatasetFields: React.FC = props => { } } - return - 字段列表 + return + + {t('field_list')} + { diff --git a/packages/graphic-walker/src/fields/fieldsContext.tsx b/packages/graphic-walker/src/fields/fieldsContext.tsx index 1764ac68..abc08f22 100644 --- a/packages/graphic-walker/src/fields/fieldsContext.tsx +++ b/packages/graphic-walker/src/fields/fieldsContext.tsx @@ -39,29 +39,20 @@ export const FieldsContextWrapper: React.FC = props => { export default FieldsContextWrapper; -export const DRAGGABLE_STATE_KEYS: Array = [ - { id: 'fields', name: '字段', mode: 0 }, - { id: 'columns', name: '列', mode: 0 }, - { id: 'rows', name: '行', mode: 0 }, - { id: 'color', name: '颜色', mode: 1 }, - { id: 'opacity', name: '透明度', mode: 1 }, - { id: 'size', name: '大小', mode: 1 }, - { id: 'shape', name: '形状', mode: 1}, - { id: 'theta', name: '角度', mode: 1 }, - { id: 'radius', name: '半径', mode: 1 } -]; +export const DRAGGABLE_STATE_KEYS: Readonly = [ + { id: 'fields', mode: 0 }, + { id: 'columns', mode: 0 }, + { id: 'rows', mode: 0 }, + { id: 'color', mode: 1 }, + { id: 'opacity', mode: 1 }, + { id: 'size', mode: 1 }, + { id: 'shape', mode: 1}, + { id: 'theta', mode: 1 }, + { id: 'radius', mode: 1 } +] as const; -export const AGGREGATOR_LIST = [ - { - value: "sum", - label: "求和", - }, - { - value: "mean", - label: "平均值", - }, - { - value: "count", - label: "计数", - }, -]; +export const AGGREGATOR_LIST: Readonly = [ + 'sum', + 'mean', + 'count', +] as const; diff --git a/packages/graphic-walker/src/fields/obComponents/obPill.tsx b/packages/graphic-walker/src/fields/obComponents/obPill.tsx index 84f79371..e8619775 100644 --- a/packages/graphic-walker/src/fields/obComponents/obPill.tsx +++ b/packages/graphic-walker/src/fields/obComponents/obPill.tsx @@ -7,6 +7,8 @@ import { IDraggableStateKey } from '../../interfaces'; import { useGlobalStore } from '../../store'; import { Pill } from '../components'; import { AGGREGATOR_LIST } from '../fieldsContext'; +import { useTranslation } from 'react-i18next'; + interface PillProps { provided: DraggableProvided; @@ -18,6 +20,8 @@ const OBPill: React.FC = props => { const { vizStore } = useGlobalStore(); const { visualConfig } = vizStore; const field = vizStore.draggableFieldState[dkey.id][fIndex]; + const { t } = useTranslation('translation', { keyPrefix: 'constant.aggregator' }); + return = props => { onChange={(e) => { vizStore.setFieldAggregator(dkey.id, fIndex, e.target.value) }} > { - AGGREGATOR_LIST.map(op => {op.label}) + AGGREGATOR_LIST.map(op => {t(op)}) } )} - {field.analyticType === 'dimension' && field.sort === 'ascending' && } - {field.analyticType === 'dimension' && field.sort === 'descending' && } + {field.analyticType === 'dimension' && field.sort === 'ascending' && } + {field.analyticType === 'dimension' && field.sort === 'descending' && } } diff --git a/packages/graphic-walker/src/fields/posFields/index.tsx b/packages/graphic-walker/src/fields/posFields/index.tsx index e888f43c..1b2f8197 100644 --- a/packages/graphic-walker/src/fields/posFields/index.tsx +++ b/packages/graphic-walker/src/fields/posFields/index.tsx @@ -26,7 +26,7 @@ const PosFields: React.FC = props => { }, [geoms[0]]) return { - channels.map(dkey => + channels.map(dkey => {(provided, snapshot) => ( diff --git a/packages/graphic-walker/src/interfaces.ts b/packages/graphic-walker/src/interfaces.ts index 6325e642..36c5f065 100644 --- a/packages/graphic-walker/src/interfaces.ts +++ b/packages/graphic-walker/src/interfaces.ts @@ -111,6 +111,5 @@ export interface DraggableFieldState { export interface IDraggableStateKey { id: keyof DraggableFieldState; - name: string; mode: number } \ No newline at end of file diff --git a/packages/graphic-walker/src/locales/en-US.json b/packages/graphic-walker/src/locales/en-US.json new file mode 100644 index 00000000..6c577f08 --- /dev/null +++ b/packages/graphic-walker/src/locales/en-US.json @@ -0,0 +1,96 @@ +{ + "constant": { + "mark_type": { + "__enum__": "Mark Type", + "auto": "Auto", + "bar": "Bar", + "line": "Polyline", + "area": "Area", + "point": "Scatter", + "circle": "Circle", + "tick": "Tick", + "rect": "Rectangle", + "arc": "Arc", + "boxplot": "Box (Box Plot)" + }, + "layout_type": { + "__enum__": "Layout Mode", + "auto": "Auto", + "fixed": "Fixed" + }, + "draggable_key": { + "fields": "Fields", + "columns": "Columns", + "rows": "Rows", + "color": "Color", + "opacity": "Opacity", + "size": "Size", + "shape": "Shape", + "theta": "Angle", + "radius": "Radius" + }, + "aggregator": { + "sum": "Sum", + "mean": "Mean", + "count": "Count" + } + }, + "App": { + "labels": { + "data_interpretation": "Interpret Data" + } + }, + "DataSource": { + "labels": { + "cur_dataset": "Current Dataset" + }, + "buttons": { + "create_dataset": "Create Dataset" + }, + "dialog": { + "create_data_source": "New Data Source", + "data_types": "Choose Import Methods", + "text_file_data": "Open Local Text File", + "public_data": "Use Public Dataset", + "data_source_type": "Data Source Type: [{{sourceType}}]", + "file": { + "open": "Open...", + "submit": "Submit", + "dataset_name": "Dataset Name" + }, + "public": { + "submit": "Submit" + } + } + }, + "main": { + "tablist": { + "new": "+ New", + "autoTitle": "Chart {{idx}}" + }, + "tabpanel": { + "settings": { + "toggle": { + "aggregation": "Enable Aggregation", + "stack": "Enable Stack", + "axes_resize": "Enable Axes Resizing", + "debug": "Enable debugging" + }, + "sort": "Sorting Order", + "button": { + "ascending": "Sort in Ascending Order", + "descending": "Sort in Descending Order", + "transpose": "Transpose" + }, + "size": "Resize", + "size_setting": { + "width": "Width", + "height": "Height" + } + }, + "DatasetFields": { + "field_list": "Field List" + } + } + } +} \ No newline at end of file diff --git a/packages/graphic-walker/src/locales/i18n.ts b/packages/graphic-walker/src/locales/i18n.ts new file mode 100644 index 00000000..f52c0188 --- /dev/null +++ b/packages/graphic-walker/src/locales/i18n.ts @@ -0,0 +1,50 @@ +import i18n, { Resource } from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; + +import localeEnUs from './en-US.json'; +import localeZhCn from './zh-CN.json'; + + +const locales: Resource & { 'en-US': any } = { + 'en': { + translation: localeEnUs, + }, + 'en-US': { + translation: localeEnUs, + }, + 'zh': { + translation: localeZhCn, + }, + 'zh-CN': { + translation: localeZhCn, + }, +} as const; + +i18n.use(initReactI18next).use(LanguageDetector).init({ + fallbackLng: 'en-US', + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, + resources: locales, +}); + +const loadedLangs: string[] = []; // exclude built-in keys to enable rewrite + +export const mergeLocaleRes = (resources: { [lang: string]: Resource }) => { + for (const lang in resources) { + if (Object.prototype.hasOwnProperty.call(resources, lang)) { + if (loadedLangs.includes(lang)) { + continue; + } + + loadedLangs.push(lang); + const resource = resources[lang]; + i18n.addResourceBundle(lang, 'translation', resource, false, true); + } + } +}; + +export const setLocaleLanguage = (lang: string) => { + return i18n.changeLanguage(lang); +}; diff --git a/packages/graphic-walker/src/locales/zh-CN.json b/packages/graphic-walker/src/locales/zh-CN.json new file mode 100644 index 00000000..c55946ef --- /dev/null +++ b/packages/graphic-walker/src/locales/zh-CN.json @@ -0,0 +1,96 @@ +{ + "constant": { + "mark_type": { + "__enum__": "标记类型", + "auto": "自动", + "bar": "条形", + "line": "折线", + "area": "面积", + "point": "散点", + "circle": "圆", + "tick": "标记", + "rect": "矩形", + "arc": "弧形", + "boxplot": "统计箱" + }, + "layout_type": { + "__enum__": "尺寸模式", + "auto": "自动", + "fixed": "固定" + }, + "draggable_key": { + "fields": "字段", + "columns": "列", + "rows": "行", + "color": "颜色", + "opacity": "透明度", + "size": "大小", + "shape": "形状", + "theta": "角度", + "radius": "半径" + }, + "aggregator": { + "sum": "求和", + "mean": "平均值", + "count": "计数" + } + }, + "App": { + "labels": { + "data_interpretation": "数据解读" + } + }, + "DataSource": { + "labels": { + "cur_dataset": "当前数据集" + }, + "buttons": { + "create_dataset": "创建数据集" + }, + "dialog": { + "create_data_source": "创建数据源", + "data_types": "选择类型", + "text_file_data": "从文件中导入", + "public_data": "公共数据集", + "data_source_type": "数据源类型:[{{sourceType}}]", + "file": { + "open": "打开文件...", + "submit": "确认", + "dataset_name": "数据集名称" + }, + "public": { + "submit": "确认" + } + } + }, + "main": { + "tablist": { + "new": "+ 新建", + "autoTitle": "图表 {{idx}}" + }, + "tabpanel": { + "settings": { + "toggle": { + "aggregation": "聚合度量", + "stack": "开启堆叠", + "axes_resize": "坐标系缩放", + "debug": "图表调试" + }, + "sort": "排序", + "button": { + "ascending": "升序排序", + "descending": "降序排序", + "transpose": "转置" + }, + "size": "调整尺寸", + "size_setting": { + "width": "宽度", + "height": "高度" + } + }, + "DatasetFields": { + "field_list": "字段列表" + } + } + } +} \ No newline at end of file diff --git a/packages/graphic-walker/src/segments/visNav.tsx b/packages/graphic-walker/src/segments/visNav.tsx index 076f8558..5c3549f2 100644 --- a/packages/graphic-walker/src/segments/visNav.tsx +++ b/packages/graphic-walker/src/segments/visNav.tsx @@ -3,20 +3,24 @@ import { observer } from "mobx-react-lite"; import PureTabs, { ITabOption } from "../components/tabs/pureTab"; import { useGlobalStore } from "../store"; + const ADD_KEY = '_add'; const VisNav: React.FC = (props) => { const { vizStore, commonStore } = useGlobalStore(); const { visIndex, visList } = vizStore; const { currentDataset } = commonStore; + const tabs: ITabOption[] = visList.map((v) => ({ key: v.visId, - label: v.name || "vis", - })) + label: v.name?.[0] || 'vis', + options: v.name?.[1], + })); + tabs.push({ key: ADD_KEY, - label: '+ 新建' - }) + label: 'main.tablist.new' + }); const visSelectionHandler = useCallback((tabKey: string, tabIndex: number) => { if (tabKey === ADD_KEY) { diff --git a/packages/graphic-walker/src/store/visualSpecStore.ts b/packages/graphic-walker/src/store/visualSpecStore.ts index 858b88cb..df26eaf5 100644 --- a/packages/graphic-walker/src/store/visualSpecStore.ts +++ b/packages/graphic-walker/src/store/visualSpecStore.ts @@ -90,7 +90,7 @@ function initEncoding(): DraggableFieldState { function initVisualConfig (): IVisualConfig { return { defaultAggregated: true, - geoms: [GEMO_TYPES[0].value], + geoms: [GEMO_TYPES[0]!], defaultStack: true, showActions: false, interactiveScale: false, @@ -104,7 +104,7 @@ function initVisualConfig (): IVisualConfig { } interface IVisSpec { - name?: string; + name?: [string, Record?]; visId: string; encodings: DraggableFieldState; config: IVisualConfig; @@ -122,7 +122,7 @@ export class VizSpecStore { this.draggableFieldState = initEncoding(); this.visualConfig = initVisualConfig(); this.visList.push({ - name: '图表 1', + name: ['main.tablist.autoTitle', { idx: 1 }], visId: uuidv4(), config: initVisualConfig(), encodings: initEncoding() @@ -166,7 +166,7 @@ export class VizSpecStore { } public addVisualization () { this.visList.push({ - name: '图表 ' + (this.visList.length + 1), + name: ['main.tablist.autoTitle', { idx: this.visList.length + 1 }], visId: uuidv4(), config: initVisualConfig(), encodings: initEncoding() @@ -183,8 +183,8 @@ export class VizSpecStore { public setVisName (visIndex: number, name: string) { this.visList[visIndex] = { ...this.visList[visIndex], - name - } + name: [name], + }; } /** * FIXME: tmp diff --git a/packages/graphic-walker/src/visualSettings/index.tsx b/packages/graphic-walker/src/visualSettings/index.tsx index 958f9c84..694ac7fe 100644 --- a/packages/graphic-walker/src/visualSettings/index.tsx +++ b/packages/graphic-walker/src/visualSettings/index.tsx @@ -6,9 +6,12 @@ import SizeSetting from '../components/sizeSetting'; import { CHART_LAYOUT_TYPE, GEMO_TYPES } from '../config'; import { useGlobalStore } from '../store'; import styled from 'styled-components' -import { ArrowPathIcon } from '@heroicons/react/24/solid' +import { ArrowPathIcon } from '@heroicons/react/24/solid'; +import { useTranslation } from 'react-i18next'; + export const LiteContainer = styled.div` + margin: 0.2em; border: 1px solid #d9d9d9; padding: 1em; background-color: #fff; @@ -17,61 +20,156 @@ export const LiteContainer = styled.div` const VisualSettings: React.FC = () => { const { vizStore } = useGlobalStore(); const { visualConfig, sortCondition } = vizStore; + const { t: tGlobal } = useTranslation(); + const { t } = useTranslation('translation', { keyPrefix: 'main.tabpanel.settings' }); + return - { - vizStore.setVisualConfig('defaultAggregated', e.target.checked); - }} /> - 聚合度量 + { + vizStore.setVisualConfig('defaultAggregated', e.target.checked); + }} + /> + + {t('toggle.aggregation')} + - { - vizStore.setVisualConfig('defaultStack', e.target.checked); - }} /> - 开启堆叠 + { + vizStore.setVisualConfig('defaultStack', e.target.checked); + }} + /> + + {t('toggle.stack')} + - - 标记类型 + + + {tGlobal('constant.mark_type.__enum__')} + { vizStore.setVisualConfig('geoms', [e.target.value]); }} > - {GEMO_TYPES.map((g) => ( - - {g.label} + {GEMO_TYPES.map(g => ( + + {tGlobal(`constant.mark_type.${g}`)} ))} - { - vizStore.setVisualConfig('interactiveScale', e.target.checked); - }} /> - 坐标系缩放 + { + vizStore.setVisualConfig('interactiveScale', e.target.checked); + }} + /> + + {t('toggle.axes_resize')} + - 排序 - { - vizStore.applyDefaultSort('ascending') - }} /> - { - vizStore.applyDefaultSort('descending'); - }} /> + + {t('sort')} + + { + vizStore.applyDefaultSort('ascending') + }} + role="button" + tabIndex={!sortCondition ? undefined : 0} + aria-disabled={!sortCondition} + xlinkTitle={t('button.ascending')} + aria-label={t('button.ascending')} + /> + { + vizStore.applyDefaultSort('descending'); + }} + role="button" + tabIndex={!sortCondition ? undefined : 0} + aria-disabled={!sortCondition} + xlinkTitle={t('button.descending')} + aria-label={t('button.descending')} + /> - 转置 - { - vizStore.transpose(); - }} /> + + {t('button.transpose')} + + { + vizStore.transpose(); + }} + /> - 尺寸模式 + + {tGlobal(`constant.layout_type.__enum__`)} + { // vizStore.setVisualConfig('geoms', [e.target.value]); @@ -80,9 +178,14 @@ const VisualSettings: React.FC = () => { }) }} > - {CHART_LAYOUT_TYPE.map((g) => ( - - {g.label} + {CHART_LAYOUT_TYPE.map(g => ( + + {tGlobal(`constant.layout_type.${g}`)} ))} @@ -104,13 +207,32 @@ const VisualSettings: React.FC = () => { }) }} /> - 尺寸大小 + + {t('size')} + - { - vizStore.setVisualConfig('showActions', e.target.checked); - }} /> - 图表调试 + { + vizStore.setVisualConfig('showActions', e.target.checked); + }} + /> + + {t('toggle.debug')} + diff --git a/packages/graphic-walker/vite.config.ts b/packages/graphic-walker/vite.config.ts index fc26d62a..99554e5d 100644 --- a/packages/graphic-walker/vite.config.ts +++ b/packages/graphic-walker/vite.config.ts @@ -6,7 +6,10 @@ import typescript from '@rollup/plugin-typescript' // https://vitejs.dev/config/ export default defineConfig({ server: { - port: 2002 + port: 2002, + proxy: { + '/datasets': 'http://localhost:8080', + }, }, plugins: [ // @ts-ignore diff --git a/packages/rath-client/src/pages/manualControl/index.tsx b/packages/rath-client/src/pages/manualControl/index.tsx index 0be0f61e..6a36b2c6 100644 --- a/packages/rath-client/src/pages/manualControl/index.tsx +++ b/packages/rath-client/src/pages/manualControl/index.tsx @@ -6,7 +6,7 @@ import { useGlobalStore } from '../../store'; import '@kanaries/graphic-walker/dist/style.css'; const VisualInterface: React.FC = props => { - const { dataSourceStore, commonStore } = useGlobalStore(); + const { dataSourceStore, commonStore, langStore } = useGlobalStore(); // TODO: discuss use clean data from dataSourceStore or cooked data from dataPipeline? const { cleanedData, mutFields } = dataSourceStore; const { graphicWalkerSpec } = commonStore @@ -21,7 +21,7 @@ const VisualInterface: React.FC = props => { } }) }, [mutFields]) - return + return } export default observer(VisualInterface); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index da025658..58985257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7544,6 +7544,13 @@ html-minifier-terser@^5.0.1: relateurl "^0.2.7" terser "^4.6.3" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-tags@^3.1.0: version "3.1.0" resolved "https://registry.nlark.com/html-tags/download/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" @@ -7701,6 +7708,20 @@ human-signals@^1.1.1: resolved "https://registry.nlark.com/human-signals/download/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha1-xbHNFPUK6uCatsWf5jujOV/k36M= +i18next-browser-languagedetector@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.5.tgz#ed8c9319a8d246995d8ec8fccb5bf5f4248d0fb1" + integrity sha512-11t7b39oKeZe4uyMxLSPnfw28BCPNLZgUk7zyufex0zKXZ+Bv+JnmJgoB+IfQLZwDt1d71PM8vwBX1NCgliY3g== + dependencies: + "@babel/runtime" "^7.18.9" + +i18next@^21.9.1: + version "21.9.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.9.1.tgz#9e3428990f5b2cc9ac1b98dd025f3e411c368249" + integrity sha512-ITbDrAjbRR73spZAiu6+ex5WNlHRr1mY+acDi2ioTHuUiviJqSz269Le1xHAf0QaQ6GgIHResUhQNcxGwa/PhA== + dependencies: + "@babel/runtime" "^7.17.2" + iconv-lite@0.4, iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.nlark.com/iconv-lite/download/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -14728,6 +14749,11 @@ vm-browserify@^1.0.1: resolved "https://registry.nlark.com/vm-browserify/download/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha1-eGQcSIuObKkadfUR56OzKobl3aA= +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.npm.taobao.org/w3c-hr-time/download/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"