@mwater/visualization 5.6.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/ColorComponent.js +2 -2
- package/lib/TranslationsTabComponent.d.ts +34 -0
- package/lib/TranslationsTabComponent.js +256 -0
- package/lib/dashboards/DashboardComponent.js +1 -1
- package/lib/dashboards/ServerDashboardDataSource.d.ts +0 -1
- package/lib/dashboards/ServerDashboardDataSource.js +0 -15
- package/lib/dashboards/SettingsModalComponent.js +9 -233
- package/lib/datagrids/DatagridComponent.js +5 -0
- package/lib/datagrids/DatagridViewComponent.js +30 -4
- package/lib/maps/BufferLayer.d.ts +0 -13
- package/lib/maps/BufferLayer.js +12 -237
- package/lib/maps/BufferLayerDesignerComponent.d.ts +1 -1
- package/lib/maps/BufferLayerDesignerComponent.js +0 -5
- package/lib/maps/ChoroplethLayer.d.ts +1 -16
- package/lib/maps/ChoroplethLayer.js +13 -358
- package/lib/maps/ClusterLayer.d.ts +0 -9
- package/lib/maps/ClusterLayer.js +0 -250
- package/lib/maps/DirectMapDataSource.js +1 -38
- package/lib/maps/GridLayer.d.ts +0 -15
- package/lib/maps/GridLayer.js +0 -212
- package/lib/maps/Layer.d.ts +1 -26
- package/lib/maps/Layer.js +0 -13
- package/lib/maps/MapComponent.d.ts +19 -35
- package/lib/maps/MapComponent.js +135 -76
- package/lib/maps/MapControlComponent.d.ts +4 -5
- package/lib/maps/MapControlComponent.js +5 -12
- package/lib/maps/MapDesign.d.ts +8 -0
- package/lib/maps/MapDesignerComponent.d.ts +2 -0
- package/lib/maps/MapDesignerComponent.js +7 -2
- package/lib/maps/MapLayerDataSource.d.ts +0 -4
- package/lib/maps/MapLayerViewDesignerComponent.d.ts +3 -1
- package/lib/maps/MapLayerViewDesignerComponent.js +5 -1
- package/lib/maps/MapLayersDesignerComponent.d.ts +2 -0
- package/lib/maps/MapLayersDesignerComponent.js +2 -1
- package/lib/maps/MapTranslationsTab.d.ts +15 -0
- package/lib/maps/MapTranslationsTab.js +47 -0
- package/lib/maps/MapUtils.d.ts +11 -0
- package/lib/maps/MapUtils.js +47 -0
- package/lib/maps/MapViewComponent.d.ts +1 -1
- package/lib/maps/MapViewComponent.js +1 -8
- package/lib/maps/MarkersLayer.d.ts +1 -14
- package/lib/maps/MarkersLayer.js +71 -252
- package/lib/maps/MarkersLayerDesign.d.ts +4 -0
- package/lib/maps/MarkersLayerDesignerComponent.d.ts +20 -16
- package/lib/maps/MarkersLayerDesignerComponent.js +77 -23
- package/lib/maps/ServerMapDataSource.d.ts +0 -1
- package/lib/maps/ServerMapDataSource.js +0 -15
- package/lib/maps/SwitchableTileUrlLayer.d.ts +0 -2
- package/lib/maps/SwitchableTileUrlLayer.js +0 -9
- package/lib/maps/TileUrlLayer.d.ts +0 -1
- package/lib/maps/TileUrlLayer.js +0 -5
- package/lib/maps/VectorMapViewComponent.js +12 -1
- package/lib/maps/vectorMaps.d.ts +5 -6
- package/lib/maps/vectorMaps.js +13 -9
- package/lib/widgets/MapWidget.js +2 -1
- package/package.json +2 -2
- package/src/ColorComponent.tsx +2 -2
- package/src/TranslationsTabComponent.tsx +429 -0
- package/src/dashboards/DashboardComponent.tsx +1 -1
- package/src/dashboards/ServerDashboardDataSource.ts +0 -19
- package/src/dashboards/SettingsModalComponent.tsx +27 -383
- package/src/datagrids/DatagridComponent.tsx +6 -0
- package/src/datagrids/DatagridViewComponent.tsx +41 -5
- package/src/maps/BufferLayer.ts +16 -262
- package/src/maps/BufferLayerDesignerComponent.tsx +0 -6
- package/src/maps/ChoroplethLayer.ts +16 -393
- package/src/maps/ClusterLayer.ts +0 -274
- package/src/maps/DirectMapDataSource.ts +2 -49
- package/src/maps/GridLayer.ts +0 -224
- package/src/maps/Layer.ts +1 -35
- package/src/maps/MapComponent.tsx +448 -0
- package/src/maps/MapControlComponent.tsx +41 -0
- package/src/maps/MapDesign.ts +6 -0
- package/src/maps/MapDesignerComponent.tsx +18 -1
- package/src/maps/MapLayerDataSource.ts +0 -5
- package/src/maps/MapLayerViewDesignerComponent.ts +9 -2
- package/src/maps/MapLayersDesignerComponent.ts +4 -1
- package/src/maps/MapTranslationsTab.tsx +53 -0
- package/src/maps/MapUtils.ts +48 -0
- package/src/maps/MapViewComponent.tsx +2 -8
- package/src/maps/MarkersLayer.ts +79 -270
- package/src/maps/MarkersLayerDesign.ts +6 -0
- package/src/maps/MarkersLayerDesignerComponent.tsx +114 -38
- package/src/maps/ServerMapDataSource.ts +0 -19
- package/src/maps/SwitchableTileUrlLayer.tsx +0 -11
- package/src/maps/TileUrlLayer.tsx +0 -6
- package/src/maps/VectorMapViewComponent.tsx +13 -2
- package/src/maps/vectorMaps.tsx +12 -9
- package/src/widgets/MapWidget.tsx +2 -0
- package/src/maps/MapComponent.ts +0 -311
- package/src/maps/MapControlComponent.ts +0 -46
- package/src/maps/RasterMapViewComponent.ts +0 -345
|
@@ -26,15 +26,6 @@ class SwitchableTileUrlLayer extends Layer_1.default {
|
|
|
26
26
|
}
|
|
27
27
|
return option.tileUrl || null;
|
|
28
28
|
}
|
|
29
|
-
/** Gets the utf grid url for definition type "TileUrl" */
|
|
30
|
-
getUtfGridUrl(design, filters) {
|
|
31
|
-
// Find active option
|
|
32
|
-
const option = design.options.find((d) => d.id === design.activeOption);
|
|
33
|
-
if (!option) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return option.utfGridUrl || null;
|
|
37
|
-
}
|
|
38
29
|
getLegend(options) {
|
|
39
30
|
const { design, name } = options;
|
|
40
31
|
// Find active option
|
|
@@ -16,7 +16,6 @@ export interface TileUrlLayerDesign {
|
|
|
16
16
|
export default class TileUrlLayer extends Layer<TileUrlLayerDesign> {
|
|
17
17
|
getLayerDefinitionType(): "TileUrl";
|
|
18
18
|
getTileUrl(design: any, filters: any): any;
|
|
19
|
-
getUtfGridUrl(design: any, filters: any): null;
|
|
20
19
|
getMinZoom(design: any): any;
|
|
21
20
|
getMaxZoom(design: any): any;
|
|
22
21
|
isEditable(): boolean;
|
package/lib/maps/TileUrlLayer.js
CHANGED
|
@@ -18,7 +18,6 @@ Design is:
|
|
|
18
18
|
legendUrl:
|
|
19
19
|
*/
|
|
20
20
|
class TileUrlLayer extends Layer_1.default {
|
|
21
|
-
// Gets the type of layer definition ("JsonQLCss"/"TileUrl")
|
|
22
21
|
getLayerDefinitionType() {
|
|
23
22
|
return "TileUrl";
|
|
24
23
|
}
|
|
@@ -26,10 +25,6 @@ class TileUrlLayer extends Layer_1.default {
|
|
|
26
25
|
getTileUrl(design, filters) {
|
|
27
26
|
return design.tileUrl;
|
|
28
27
|
}
|
|
29
|
-
// Gets the utf grid url for definition type "TileUrl"
|
|
30
|
-
getUtfGridUrl(design, filters) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
28
|
// Get min and max zoom levels
|
|
34
29
|
getMinZoom(design) {
|
|
35
30
|
return design.minZoom;
|
|
@@ -67,7 +67,18 @@ function VectorMapViewComponent(props) {
|
|
|
67
67
|
// Locale to use
|
|
68
68
|
const locale = props.locale || props.design.locale || "en";
|
|
69
69
|
// Translate function to use
|
|
70
|
-
const translate =
|
|
70
|
+
const translate = (0, react_1.useCallback)((input) => {
|
|
71
|
+
// Use passed in translate function if present
|
|
72
|
+
if (props.translate) {
|
|
73
|
+
return props.translate(input);
|
|
74
|
+
}
|
|
75
|
+
// If locale is the same as the design locale, don't translate
|
|
76
|
+
if (locale === props.design.locale) {
|
|
77
|
+
return input;
|
|
78
|
+
}
|
|
79
|
+
// Otherwise, use translation from design
|
|
80
|
+
return props.design.translations?.[locale]?.[input] ?? input;
|
|
81
|
+
}, [props.translate, props.design.translations, props.design.locale, locale]);
|
|
71
82
|
// Last feature that mouse entered
|
|
72
83
|
const lastFeature = (0, react_2.useRef)();
|
|
73
84
|
// Load map
|
package/lib/maps/vectorMaps.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { Map, LngLatBoundsLike } from "maplibre-gl";
|
|
1
|
+
import maplibregl, { Map, LngLatBoundsLike } from "maplibre-gl";
|
|
2
2
|
import "maplibre-gl/dist/maplibre-gl.css";
|
|
3
3
|
import "./VectorMapViewComponent.css";
|
|
4
4
|
import React from "react";
|
|
5
|
+
export { default as maplibregl } from "maplibre-gl";
|
|
5
6
|
export declare function setPrintingModeEnabled(val: boolean): void;
|
|
6
7
|
/** This must be called to set the appropriate key before use. If it is not set, vector maps will not function.
|
|
7
8
|
* Maps will fall back to leaflet if the key is not set or if set to ""
|
|
8
9
|
*/
|
|
9
10
|
export declare function setMapTilerApiKey(key: string): void;
|
|
10
11
|
export declare function getMapTilerApiKey(): string;
|
|
11
|
-
/** Check if vector maps are enabled by setting API key */
|
|
12
|
-
export declare function areVectorMapsEnabled(): boolean;
|
|
13
12
|
export type BaseLayer = "bing_road" | "bing_aerial" | "cartodb_positron" | "cartodb_dark_matter" | "blank";
|
|
14
13
|
/** Loads a vector map, refreshing the WebGL context as needed */
|
|
15
14
|
export declare function useVectorMap(options: {
|
|
@@ -19,7 +18,7 @@ export declare function useVectorMap(options: {
|
|
|
19
18
|
dragPan?: boolean;
|
|
20
19
|
touchZoomRotate?: boolean;
|
|
21
20
|
padding?: number;
|
|
22
|
-
}): Map | undefined;
|
|
21
|
+
}): maplibregl.Map | undefined;
|
|
23
22
|
/** Sets cursor as pointer when over any layers with the specified ids */
|
|
24
23
|
export declare function useHoverCursor(map: maplibregl.Map | undefined, layerIds: string[]): void;
|
|
25
24
|
/** Apply user style to a map with base style */
|
|
@@ -30,9 +29,9 @@ export declare function useStyleMap(options: {
|
|
|
30
29
|
userStyle: maplibregl.StyleSpecification | null | undefined;
|
|
31
30
|
}): void;
|
|
32
31
|
/** Loads a base style for the map */
|
|
33
|
-
export declare function useBaseStyle(baseLayer: BaseLayer):
|
|
32
|
+
export declare function useBaseStyle(baseLayer: BaseLayer): maplibregl.StyleSpecification | null;
|
|
34
33
|
/** Combines a base style and a user style */
|
|
35
|
-
export declare function mergeBaseAndUserStyle(baseStyle: maplibregl.StyleSpecification | null | undefined, userStyle: maplibregl.StyleSpecification | null | undefined, baseLayerOpacity?: number | null):
|
|
34
|
+
export declare function mergeBaseAndUserStyle(baseStyle: maplibregl.StyleSpecification | null | undefined, userStyle: maplibregl.StyleSpecification | null | undefined, baseLayerOpacity?: number | null): maplibregl.StyleSpecification | null;
|
|
36
35
|
export declare function AttributionControl(props: {
|
|
37
36
|
baseLayer: BaseLayer;
|
|
38
37
|
extraText?: string;
|
package/lib/maps/vectorMaps.js
CHANGED
|
@@ -3,10 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.maplibregl = void 0;
|
|
6
7
|
exports.setPrintingModeEnabled = setPrintingModeEnabled;
|
|
7
8
|
exports.setMapTilerApiKey = setMapTilerApiKey;
|
|
8
9
|
exports.getMapTilerApiKey = getMapTilerApiKey;
|
|
9
|
-
exports.areVectorMapsEnabled = areVectorMapsEnabled;
|
|
10
10
|
exports.useVectorMap = useVectorMap;
|
|
11
11
|
exports.useHoverCursor = useHoverCursor;
|
|
12
12
|
exports.useStyleMap = useStyleMap;
|
|
@@ -21,6 +21,9 @@ const mapSymbols_1 = require("./mapSymbols");
|
|
|
21
21
|
require("maplibre-gl/dist/maplibre-gl.css");
|
|
22
22
|
require("./VectorMapViewComponent.css");
|
|
23
23
|
const react_2 = __importDefault(require("react"));
|
|
24
|
+
// Re-export maplibregl for consumers
|
|
25
|
+
var maplibre_gl_2 = require("maplibre-gl");
|
|
26
|
+
Object.defineProperty(exports, "maplibregl", { enumerable: true, get: function () { return __importDefault(maplibre_gl_2).default; } });
|
|
24
27
|
/** Set to true to enable printing by preserving the drawing buffer */
|
|
25
28
|
let printingModeEnabled = false;
|
|
26
29
|
function setPrintingModeEnabled(val) {
|
|
@@ -37,10 +40,6 @@ function setMapTilerApiKey(key) {
|
|
|
37
40
|
function getMapTilerApiKey() {
|
|
38
41
|
return mapTilerApiKey;
|
|
39
42
|
}
|
|
40
|
-
/** Check if vector maps are enabled by setting API key */
|
|
41
|
-
function areVectorMapsEnabled() {
|
|
42
|
-
return mapTilerApiKey !== "";
|
|
43
|
-
}
|
|
44
43
|
/** Loads a vector map, refreshing the WebGL context as needed */
|
|
45
44
|
function useVectorMap(options) {
|
|
46
45
|
const { divRef, bounds, scrollZoom, dragPan, touchZoomRotate, padding } = options;
|
|
@@ -95,7 +94,10 @@ function useVectorMap(options) {
|
|
|
95
94
|
[-179.9, -85], // Southwest coordinates
|
|
96
95
|
[179.9, 85] // Northeast coordinates
|
|
97
96
|
],
|
|
98
|
-
|
|
97
|
+
// In maplibre-gl v5+, WebGL context options must be in canvasContextAttributes
|
|
98
|
+
canvasContextAttributes: {
|
|
99
|
+
preserveDrawingBuffer: printingModeEnabled
|
|
100
|
+
}
|
|
99
101
|
};
|
|
100
102
|
if (bounds) {
|
|
101
103
|
mapConstructorOptions.bounds = bounds;
|
|
@@ -124,10 +126,12 @@ function useVectorMap(options) {
|
|
|
124
126
|
// Check if known
|
|
125
127
|
const mapSymbol = (0, mapSymbols_1.getMapSymbols)().find((s) => s.value == ev.id);
|
|
126
128
|
if (mapSymbol) {
|
|
127
|
-
m.loadImage(mapSymbol.url
|
|
128
|
-
if (
|
|
129
|
-
m.addImage(mapSymbol.value,
|
|
129
|
+
m.loadImage(mapSymbol.url).then((response) => {
|
|
130
|
+
if (response && response.data && !m.hasImage(mapSymbol.value)) {
|
|
131
|
+
m.addImage(mapSymbol.value, response.data, { sdf: true });
|
|
130
132
|
}
|
|
133
|
+
}).catch((err) => {
|
|
134
|
+
console.error("Error loading map symbol:", err);
|
|
131
135
|
});
|
|
132
136
|
}
|
|
133
137
|
});
|
package/lib/widgets/MapWidget.js
CHANGED
|
@@ -91,7 +91,8 @@ class MapWidgetComponent extends react_1.default.Component {
|
|
|
91
91
|
// Require here to prevent server require problems
|
|
92
92
|
const MapDesignerComponent = require("../maps/MapDesignerComponent").default;
|
|
93
93
|
// Create editor
|
|
94
|
-
|
|
94
|
+
// Note: enableTranslations is false because translations are managed at the dashboard level
|
|
95
|
+
const editor = react_1.default.createElement(MapDesignerComponent, { schema: this.props.schema, dataSource: this.props.dataSource, design: this.state.editDesign, onDesignChange: this.handleEditDesignChange, filters: this.props.filters, enableTranslations: false });
|
|
95
96
|
// Create map (maxing out at half of width of screen)
|
|
96
97
|
const width = Math.min(document.body.clientWidth / 2, this.props.width || 0);
|
|
97
98
|
const height = ((this.props.height || 0) * width) / (this.props.width || 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mwater/visualization",
|
|
3
|
-
"version": "5.6.
|
|
3
|
+
"version": "5.6.1",
|
|
4
4
|
"description": "Visualization library",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"leaflet": "^1.8.0",
|
|
46
46
|
"lodash": "^3.1.0",
|
|
47
47
|
"lru-cache": "^6.0.0",
|
|
48
|
-
"maplibre-gl": "^
|
|
48
|
+
"maplibre-gl": "^5.16.0",
|
|
49
49
|
"markdown-it": "^12.0.4",
|
|
50
50
|
"moment": "^2.29.1",
|
|
51
51
|
"p-queue": "^8.0.1",
|
package/src/ColorComponent.tsx
CHANGED
|
@@ -51,8 +51,8 @@ export default class ColorComponent extends React.Component<ColorComponentProps,
|
|
|
51
51
|
|
|
52
52
|
const popupPosition: CSSProperties = {
|
|
53
53
|
position: "fixed",
|
|
54
|
-
top: `${rect.bottom
|
|
55
|
-
left: `${rect.left
|
|
54
|
+
top: `${rect.bottom}px`,
|
|
55
|
+
left: `${rect.left}px`,
|
|
56
56
|
zIndex: 1070,
|
|
57
57
|
backgroundColor: "white",
|
|
58
58
|
border: "solid 1px #DDD",
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import _ from "lodash"
|
|
2
|
+
import React, { useMemo, useRef, useState } from "react"
|
|
3
|
+
import { languages } from "./languages"
|
|
4
|
+
import { default as ReactSelect } from "react-select"
|
|
5
|
+
import { LocalizedString } from "@mwater/expressions"
|
|
6
|
+
import produce from "immer"
|
|
7
|
+
import FileSaver from "file-saver"
|
|
8
|
+
import * as localizeUtils from "ez-localize/lib/utils"
|
|
9
|
+
import { canAutoTranslate, translateStrings } from "./autotranslate"
|
|
10
|
+
import { FormGroup } from "@mwater/react-library/lib/bootstrap"
|
|
11
|
+
|
|
12
|
+
export interface TranslationsTabComponentProps {
|
|
13
|
+
/** Base locale of the design (e.g. "en") */
|
|
14
|
+
locale: string
|
|
15
|
+
|
|
16
|
+
/** Other locales the design is translated into */
|
|
17
|
+
otherLocales: string[]
|
|
18
|
+
|
|
19
|
+
/** Translation mappings per locale. Maps locale to { originalString: translatedString } */
|
|
20
|
+
translations: { [locale: string]: { [key: string]: string } }
|
|
21
|
+
|
|
22
|
+
/** All strings that need translation */
|
|
23
|
+
translatableStrings: string[]
|
|
24
|
+
|
|
25
|
+
/** Called when base locale changes */
|
|
26
|
+
onLocaleChange: (locale: string) => void
|
|
27
|
+
|
|
28
|
+
/** Called when other locales change */
|
|
29
|
+
onOtherLocalesChange: (locales: string[]) => void
|
|
30
|
+
|
|
31
|
+
/** Called when translations change */
|
|
32
|
+
onTranslationsChange: (translations: { [locale: string]: { [key: string]: string } }) => void
|
|
33
|
+
|
|
34
|
+
/** Custom filename for download. Defaults to "Translations.xlsx" */
|
|
35
|
+
downloadFilename?: string
|
|
36
|
+
|
|
37
|
+
/** Custom description for base language section */
|
|
38
|
+
baseLanguageDescription?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Reusable translations tab component for managing localization of designs
|
|
43
|
+
* (dashboards, maps, etc.) that have locale, otherLocales, and translations properties.
|
|
44
|
+
*/
|
|
45
|
+
export function TranslationsTabComponent(props: TranslationsTabComponentProps) {
|
|
46
|
+
const {
|
|
47
|
+
locale,
|
|
48
|
+
otherLocales,
|
|
49
|
+
translations,
|
|
50
|
+
translatableStrings,
|
|
51
|
+
onLocaleChange,
|
|
52
|
+
onOtherLocalesChange,
|
|
53
|
+
onTranslationsChange,
|
|
54
|
+
downloadFilename = "Translations.xlsx",
|
|
55
|
+
baseLanguageDescription = T`This is the base language.`
|
|
56
|
+
} = props
|
|
57
|
+
|
|
58
|
+
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
59
|
+
|
|
60
|
+
const localeOptions = useMemo(() => {
|
|
61
|
+
return _.sortBy(
|
|
62
|
+
_.map(languages, (language) => ({
|
|
63
|
+
value: language.code,
|
|
64
|
+
label: `${language.en} (${language.name})`
|
|
65
|
+
})),
|
|
66
|
+
'label'
|
|
67
|
+
)
|
|
68
|
+
}, [])
|
|
69
|
+
|
|
70
|
+
// Get available languages that aren't already selected
|
|
71
|
+
const availableLocaleOptions = useMemo(() => {
|
|
72
|
+
const selectedLocales = new Set([locale, ...otherLocales])
|
|
73
|
+
return localeOptions.filter(opt => !selectedLocales.has(opt.value))
|
|
74
|
+
}, [localeOptions, locale, otherLocales])
|
|
75
|
+
|
|
76
|
+
// Calculate percentage of strings translated for each locale
|
|
77
|
+
const translationPercentages = useMemo(() => {
|
|
78
|
+
const percentages: { [locale: string]: number } = {}
|
|
79
|
+
const totalStrings = translatableStrings.length
|
|
80
|
+
|
|
81
|
+
for (const loc of otherLocales) {
|
|
82
|
+
const translatedCount = translatableStrings.filter(str =>
|
|
83
|
+
translations?.[loc]?.[str] != null
|
|
84
|
+
).length
|
|
85
|
+
|
|
86
|
+
// Round down to nearest percent
|
|
87
|
+
percentages[loc] = (totalStrings > 0) ? Math.floor((translatedCount / totalStrings) * 100) : 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return percentages
|
|
91
|
+
}, [translations, otherLocales, translatableStrings])
|
|
92
|
+
|
|
93
|
+
const handleAddLocale = (selectedLocale: any) => {
|
|
94
|
+
const newOtherLocales = [...otherLocales, selectedLocale.value]
|
|
95
|
+
onOtherLocalesChange(newOtherLocales)
|
|
96
|
+
// Note: We don't initialize translations here because calling both callbacks
|
|
97
|
+
// in sequence would cause a race condition where the second overwrites the first.
|
|
98
|
+
// The code handles undefined translations via optional chaining.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const handleRemoveLocale = (localeToRemove: string) => {
|
|
102
|
+
const newOtherLocales = otherLocales.filter(loc => loc !== localeToRemove)
|
|
103
|
+
onOtherLocalesChange(newOtherLocales)
|
|
104
|
+
// Note: We don't remove translations here for the same reason as above.
|
|
105
|
+
// Orphaned translations are harmless and will be cleaned up on next save.
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Convert translations to LocalizedString format
|
|
109
|
+
const getLocalizedStrings = (): LocalizedString[] => {
|
|
110
|
+
const localizedStrings: LocalizedString[] = []
|
|
111
|
+
for (const str of translatableStrings) {
|
|
112
|
+
const localizedString: LocalizedString = { _base: locale }
|
|
113
|
+
localizedString[locale] = str
|
|
114
|
+
|
|
115
|
+
// Only add translations for other locales if they exist
|
|
116
|
+
for (const otherLocale of otherLocales) {
|
|
117
|
+
if (translations?.[otherLocale]?.[str]) {
|
|
118
|
+
localizedString[otherLocale] = translations[otherLocale][str]
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
localizedStrings.push(localizedString)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return localizedStrings
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Convert LocalizedString format back to translations
|
|
129
|
+
const updateFromLocalizedStrings = (localizedStrings: LocalizedString[]) => {
|
|
130
|
+
const newTranslations: { [locale: string]: { [key: string]: string } } = {}
|
|
131
|
+
|
|
132
|
+
// Initialize translations object
|
|
133
|
+
for (const loc of otherLocales) {
|
|
134
|
+
newTranslations[loc] = {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add all translations
|
|
138
|
+
for (const str of localizedStrings) {
|
|
139
|
+
for (const loc of otherLocales) {
|
|
140
|
+
if (str[loc]) {
|
|
141
|
+
newTranslations[loc][str[str._base]] = str[loc]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return newTranslations
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const handleDownload = () => {
|
|
150
|
+
// Get strings in LocalizedString format
|
|
151
|
+
const strings = getLocalizedStrings()
|
|
152
|
+
|
|
153
|
+
// Create xlsx base64 using all locales (primary + other)
|
|
154
|
+
const locales = [{ code: locale, name: locale }]
|
|
155
|
+
.concat(otherLocales.map(code => ({ code, name: code })))
|
|
156
|
+
|
|
157
|
+
const base64 = localizeUtils.exportXlsx(locales, strings)
|
|
158
|
+
|
|
159
|
+
// Download
|
|
160
|
+
FileSaver.saveAs(
|
|
161
|
+
b64toBlob(base64, "application/octet-stream"),
|
|
162
|
+
downloadFilename
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const handleUpload = () => {
|
|
167
|
+
fileInputRef.current?.click()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const handleUploadChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
|
171
|
+
const reader = new FileReader()
|
|
172
|
+
|
|
173
|
+
reader.onload = (file) => {
|
|
174
|
+
if (!file.target?.result) {
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const base64 = (file.target.result as string).split(",")[1]
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
// Create locales array for import
|
|
182
|
+
const locales = [{ code: locale, name: locale }]
|
|
183
|
+
.concat(otherLocales.map(code => ({ code, name: code })))
|
|
184
|
+
|
|
185
|
+
// Import updates
|
|
186
|
+
const updates = localizeUtils.importXlsx(locales, base64)
|
|
187
|
+
|
|
188
|
+
// If nothing localized
|
|
189
|
+
if (updates.length === 0) {
|
|
190
|
+
alert(T`No translation data found in file`)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Convert back to translations format and update
|
|
195
|
+
const newTranslations = updateFromLocalizedStrings(updates)
|
|
196
|
+
onTranslationsChange(newTranslations)
|
|
197
|
+
|
|
198
|
+
alert(T`${updates.length} translations applied`)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error("Invalid xlsx file:", error)
|
|
201
|
+
alert(T`Invalid xlsx file`)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (evt.target.files?.[0]) {
|
|
206
|
+
reader.readAsDataURL(evt.target.files[0])
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const handleTranslationsChangeForLocale = (loc: string, newLocaleTranslations: { [key: string]: string }) => {
|
|
211
|
+
const newTranslations = { ...translations }
|
|
212
|
+
newTranslations[loc] = newLocaleTranslations
|
|
213
|
+
onTranslationsChange(newTranslations)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<>
|
|
218
|
+
<FormGroup
|
|
219
|
+
label={T`Base Language`}
|
|
220
|
+
labelMuted={true}
|
|
221
|
+
help={baseLanguageDescription}
|
|
222
|
+
>
|
|
223
|
+
<ReactSelect
|
|
224
|
+
value={_.findWhere(localeOptions, { value: locale }) || null}
|
|
225
|
+
options={localeOptions}
|
|
226
|
+
onChange={(selected: any) => onLocaleChange(selected.value)}
|
|
227
|
+
/>
|
|
228
|
+
</FormGroup>
|
|
229
|
+
<FormGroup
|
|
230
|
+
label={T`Additional Languages`}
|
|
231
|
+
labelMuted={true}
|
|
232
|
+
help={T`Add languages to translate into`}
|
|
233
|
+
>
|
|
234
|
+
{/* Show current additional languages */}
|
|
235
|
+
<table>
|
|
236
|
+
<tbody>
|
|
237
|
+
{otherLocales.map(loc => {
|
|
238
|
+
const localeOption = _.findWhere(localeOptions, { value: loc })
|
|
239
|
+
if (!localeOption) {
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
<tr key={loc}>
|
|
245
|
+
<td style={{ paddingRight: 10 }}>{localeOption.label}</td>
|
|
246
|
+
<td className={translationPercentages[loc] === 100 ? "text-success" : "text-warning"} style={{ textAlign: "right" }}>
|
|
247
|
+
{T`${translationPercentages[loc]}% translated`}
|
|
248
|
+
</td>
|
|
249
|
+
<td>
|
|
250
|
+
{translationPercentages[loc] < 100 && (
|
|
251
|
+
<AutoTranslateLink
|
|
252
|
+
locale={loc}
|
|
253
|
+
baseLocale={locale}
|
|
254
|
+
strings={translatableStrings}
|
|
255
|
+
existingTranslations={translations?.[loc] || {}}
|
|
256
|
+
onTranslationsChange={(newLocaleTranslations) => handleTranslationsChangeForLocale(loc, newLocaleTranslations)}
|
|
257
|
+
/>
|
|
258
|
+
)}
|
|
259
|
+
</td>
|
|
260
|
+
<td>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
className="btn btn-sm btn-link"
|
|
264
|
+
onClick={() => handleRemoveLocale(loc)}
|
|
265
|
+
>
|
|
266
|
+
<i className="fa fa-times" />
|
|
267
|
+
</button>
|
|
268
|
+
</td>
|
|
269
|
+
</tr>
|
|
270
|
+
)
|
|
271
|
+
})}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
|
|
275
|
+
{/* Add new language dropdown */}
|
|
276
|
+
{availableLocaleOptions.length > 0 && (
|
|
277
|
+
<div className="mt-3">
|
|
278
|
+
<ReactSelect
|
|
279
|
+
value={null}
|
|
280
|
+
options={availableLocaleOptions}
|
|
281
|
+
onChange={handleAddLocale}
|
|
282
|
+
placeholder={T`Add language...`}
|
|
283
|
+
/>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</FormGroup>
|
|
287
|
+
|
|
288
|
+
{/* Add translation management section if there are additional languages */}
|
|
289
|
+
{otherLocales.length > 0 && (
|
|
290
|
+
<>
|
|
291
|
+
<FormGroup
|
|
292
|
+
label={T`Manage Translations`}
|
|
293
|
+
labelMuted={true}
|
|
294
|
+
help={T`Download and re-upload an Excel spreadsheet of text to translate:`}
|
|
295
|
+
>
|
|
296
|
+
<p>{T`Download and re-upload an Excel spreadsheet of text to translate:`}</p>
|
|
297
|
+
|
|
298
|
+
<div>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
className="btn btn-secondary"
|
|
302
|
+
onClick={handleDownload}
|
|
303
|
+
>
|
|
304
|
+
<i className="fas fa-download me-2" />{T`Download XLSX`}
|
|
305
|
+
</button>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="text-muted mt-2">
|
|
308
|
+
{T`This creates a spreadsheet that can be sent to a translator. Please do not change the first column or first row of the spreadsheet.`}
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<br />
|
|
312
|
+
<p>
|
|
313
|
+
{T`Once translation is complete, upload the file back using the button below:`}
|
|
314
|
+
</p>
|
|
315
|
+
<div>
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
className="btn btn-secondary"
|
|
319
|
+
onClick={handleUpload}
|
|
320
|
+
>
|
|
321
|
+
<i className="fas fa-upload me-2" />{T`Upload Translated XLSX`}
|
|
322
|
+
</button>
|
|
323
|
+
</div>
|
|
324
|
+
<input
|
|
325
|
+
type="file"
|
|
326
|
+
ref={fileInputRef}
|
|
327
|
+
style={{ display: "none" }}
|
|
328
|
+
onChange={handleUploadChange}
|
|
329
|
+
accept=".xlsx"
|
|
330
|
+
/>
|
|
331
|
+
</FormGroup>
|
|
332
|
+
</>
|
|
333
|
+
)}
|
|
334
|
+
</>
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
interface AutoTranslateLinkProps {
|
|
339
|
+
locale: string
|
|
340
|
+
baseLocale: string
|
|
341
|
+
strings: string[]
|
|
342
|
+
existingTranslations: { [key: string]: string }
|
|
343
|
+
onTranslationsChange: (newTranslations: { [key: string]: string }) => void
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Button that auto-translates untranslated strings using the translation service
|
|
348
|
+
*/
|
|
349
|
+
function AutoTranslateLink(props: AutoTranslateLinkProps) {
|
|
350
|
+
const { locale, baseLocale, strings, existingTranslations, onTranslationsChange } = props
|
|
351
|
+
const [isTranslating, setIsTranslating] = useState(false)
|
|
352
|
+
|
|
353
|
+
const untranslatedStrings = useMemo(() => {
|
|
354
|
+
return strings.filter(str => !existingTranslations[str])
|
|
355
|
+
}, [strings, existingTranslations])
|
|
356
|
+
|
|
357
|
+
const handleClick = async () => {
|
|
358
|
+
if (isTranslating) {
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
setIsTranslating(true)
|
|
363
|
+
try {
|
|
364
|
+
const translatedStrings = await translateStrings(untranslatedStrings, baseLocale, locale)
|
|
365
|
+
|
|
366
|
+
const newTranslations = { ...existingTranslations }
|
|
367
|
+
for (let i = 0; i < untranslatedStrings.length; i++) {
|
|
368
|
+
newTranslations[untranslatedStrings[i]] = translatedStrings[i]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
onTranslationsChange(newTranslations)
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error("Error translating strings:", error)
|
|
374
|
+
alert(T`Error translating strings`)
|
|
375
|
+
} finally {
|
|
376
|
+
setIsTranslating(false)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (untranslatedStrings.length === 0) {
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!canAutoTranslate(locale)) {
|
|
385
|
+
return null
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<button
|
|
390
|
+
type="button"
|
|
391
|
+
className="btn btn-sm btn-link"
|
|
392
|
+
onClick={handleClick}
|
|
393
|
+
disabled={isTranslating}
|
|
394
|
+
style={{ marginLeft: 5, padding: "0 5px" }}
|
|
395
|
+
>
|
|
396
|
+
{isTranslating ? (
|
|
397
|
+
<span>
|
|
398
|
+
<i className="fa fa-spinner fa-spin" />
|
|
399
|
+
{" "}
|
|
400
|
+
{T`Translating...`}
|
|
401
|
+
</span>
|
|
402
|
+
) : (
|
|
403
|
+
T`Autotranslate`
|
|
404
|
+
)}
|
|
405
|
+
</button>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Helper function for base64 to blob conversion
|
|
411
|
+
*/
|
|
412
|
+
function b64toBlob(b64Data: string, contentType: string = "", sliceSize: number = 512) {
|
|
413
|
+
const byteCharacters = atob(b64Data)
|
|
414
|
+
const byteArrays = []
|
|
415
|
+
|
|
416
|
+
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
417
|
+
const slice = byteCharacters.slice(offset, offset + sliceSize)
|
|
418
|
+
const byteNumbers = new Array(slice.length)
|
|
419
|
+
|
|
420
|
+
for (let i = 0; i < slice.length; i++) {
|
|
421
|
+
byteNumbers[i] = slice.charCodeAt(i)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const byteArray = new Uint8Array(byteNumbers)
|
|
425
|
+
byteArrays.push(byteArray)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return new Blob(byteArrays, { type: contentType })
|
|
429
|
+
}
|
|
@@ -289,7 +289,7 @@ export default class DashboardComponent extends React.Component<DashboardCompone
|
|
|
289
289
|
{this.state.locale}
|
|
290
290
|
</a>
|
|
291
291
|
<ul className="dropdown-menu dropdown-menu-end">
|
|
292
|
-
{[this.props.design.locale || "en", ...this.props.design.otherLocales].map(locale =>
|
|
292
|
+
{[...new Set([this.props.design.locale || "en", ...this.props.design.otherLocales])].map(locale =>
|
|
293
293
|
<li key={locale}>
|
|
294
294
|
<a
|
|
295
295
|
className="dropdown-item"
|
|
@@ -318,25 +318,6 @@ class ServerWidgetLayerDataSource implements MapLayerDataSource {
|
|
|
318
318
|
return this.createUrl(filters, "png")
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
// Get the url for the interactivity tiles with the specified filters applied
|
|
322
|
-
// Called with (design, filters) where design is the layer design and filters are filters to apply. Returns URL
|
|
323
|
-
getUtfGridUrl(design: any, filters: JsonQLFilter[]) {
|
|
324
|
-
// Handle special cases
|
|
325
|
-
if (this.options.layerView.type === "MWaterServer") {
|
|
326
|
-
return this.createLegacyUrl(this.options.layerView.design, "grid.json", filters)
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Create layer
|
|
330
|
-
const layer = LayerFactory.createLayer(this.options.layerView.type)
|
|
331
|
-
|
|
332
|
-
// If layer has tiles url directly available
|
|
333
|
-
if (layer.getLayerDefinitionType() === "TileUrl") {
|
|
334
|
-
return layer.getUtfGridUrl(this.options.layerView.design, filters)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return this.createUrl(filters, "grid.json")
|
|
338
|
-
}
|
|
339
|
-
|
|
340
321
|
/** Get the url for vector tile source with an expiry time. Only for layers of type "VectorTile"
|
|
341
322
|
* @param createdAfter ISO 8601 timestamp requiring that tile source on server is created after specified datetime
|
|
342
323
|
*/
|