Add react-i18next for multi-language support (#917)

This is a rough start on adding react-i18next. I'll be working on adding
more translatable strings and translations in the coming days. I'm going
to need to wrap class components in HOCs, so let me know if there's
something I should be fixing before doing that. I'm thinking now to keep
the exported class names exactly the same, and rename the existing
classes by prefixing an `I` (for internal). For example:

```
export default class AppToolbar ...
```

becomes

```
class IAppToolbar ...
const AppToolbar = withTranslation()(IAppToolbar);
export default AppToolbar;
```

I'll be able to contribute Japanese strings (I've talked to a couple
people on my team and they'll be happy to help as well), so that's the
language I decided to go with in this PR.

Closes #746

---------

Co-authored-by: Ko Nagase <nagase@georepublic.co.jp>
Co-authored-by: Harel M <harel.mazor@gmail.com>
This commit is contained in:
Keitaroh Kobayashi
2024-08-19 18:43:04 +09:00
committed by GitHub
parent 35840409b8
commit 58edd262b0
55 changed files with 2333 additions and 501 deletions
+48 -25
View File
@@ -19,31 +19,45 @@ import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? layout[type] : layout.invalid;
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
return layout[type] ? {
...layout[type],
groups: layout[type].groups.map(group => {
return {
...group,
id: group.title.replace(/ /g, "_"),
title: t(group.title)
};
}),
} : layout.invalid;
}
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = {
title: 'Layer',
id: 'layer',
title: t('Layer'),
type: 'layer'
}
const filterGroup = {
title: 'Filter',
id: 'filter',
title: t('Filter'),
type: 'filter'
}
const editorGroup = {
title: 'JSON Editor',
id: 'jsoneditor',
title: t('JSON Editor'),
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat(getLayoutForType(layerType, t).groups)
.concat([editorGroup])
}
type LayerEditorProps = {
type LayerEditorInternalProps = {
layer: LayerSpecification
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
@@ -58,14 +72,14 @@ type LayerEditorProps = {
isLastLayer?: boolean
layerIndex: number
errors?: any[]
};
} & WithTranslation;
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
@@ -76,22 +90,22 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorProps) {
constructor(props: LayerEditorInternalProps) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type).forEach(group => {
layoutGroups(this.props.layer.type, props.t).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type).groups.forEach(group => {
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -242,17 +256,19 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => {
const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.title.replace(/ /g, "_");
const groupId = group.id;
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={group.title}
key={groupId}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
@@ -265,25 +281,25 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: "Delete",
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: "Duplicate",
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: "Move layer up",
text: t("Move layer up"),
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: "Move layer down",
text: t("Move layer down"),
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
@@ -297,12 +313,12 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
return <section className="maputnik-layer-editor"
role="main"
aria-label="Layer editor"
aria-label={t("Layer editor")}
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
</h2>
<div className="layer-header__info">
<Wrapper
@@ -310,7 +326,11 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
onSelection={handleSelection}
closeOnSelection={false}
>
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
@@ -340,3 +360,6 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
</section>
}
}
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;