@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.
Files changed (92) hide show
  1. package/lib/ColorComponent.js +2 -2
  2. package/lib/TranslationsTabComponent.d.ts +34 -0
  3. package/lib/TranslationsTabComponent.js +256 -0
  4. package/lib/dashboards/DashboardComponent.js +1 -1
  5. package/lib/dashboards/ServerDashboardDataSource.d.ts +0 -1
  6. package/lib/dashboards/ServerDashboardDataSource.js +0 -15
  7. package/lib/dashboards/SettingsModalComponent.js +9 -233
  8. package/lib/datagrids/DatagridComponent.js +5 -0
  9. package/lib/datagrids/DatagridViewComponent.js +30 -4
  10. package/lib/maps/BufferLayer.d.ts +0 -13
  11. package/lib/maps/BufferLayer.js +12 -237
  12. package/lib/maps/BufferLayerDesignerComponent.d.ts +1 -1
  13. package/lib/maps/BufferLayerDesignerComponent.js +0 -5
  14. package/lib/maps/ChoroplethLayer.d.ts +1 -16
  15. package/lib/maps/ChoroplethLayer.js +13 -358
  16. package/lib/maps/ClusterLayer.d.ts +0 -9
  17. package/lib/maps/ClusterLayer.js +0 -250
  18. package/lib/maps/DirectMapDataSource.js +1 -38
  19. package/lib/maps/GridLayer.d.ts +0 -15
  20. package/lib/maps/GridLayer.js +0 -212
  21. package/lib/maps/Layer.d.ts +1 -26
  22. package/lib/maps/Layer.js +0 -13
  23. package/lib/maps/MapComponent.d.ts +19 -35
  24. package/lib/maps/MapComponent.js +135 -76
  25. package/lib/maps/MapControlComponent.d.ts +4 -5
  26. package/lib/maps/MapControlComponent.js +5 -12
  27. package/lib/maps/MapDesign.d.ts +8 -0
  28. package/lib/maps/MapDesignerComponent.d.ts +2 -0
  29. package/lib/maps/MapDesignerComponent.js +7 -2
  30. package/lib/maps/MapLayerDataSource.d.ts +0 -4
  31. package/lib/maps/MapLayerViewDesignerComponent.d.ts +3 -1
  32. package/lib/maps/MapLayerViewDesignerComponent.js +5 -1
  33. package/lib/maps/MapLayersDesignerComponent.d.ts +2 -0
  34. package/lib/maps/MapLayersDesignerComponent.js +2 -1
  35. package/lib/maps/MapTranslationsTab.d.ts +15 -0
  36. package/lib/maps/MapTranslationsTab.js +47 -0
  37. package/lib/maps/MapUtils.d.ts +11 -0
  38. package/lib/maps/MapUtils.js +47 -0
  39. package/lib/maps/MapViewComponent.d.ts +1 -1
  40. package/lib/maps/MapViewComponent.js +1 -8
  41. package/lib/maps/MarkersLayer.d.ts +1 -14
  42. package/lib/maps/MarkersLayer.js +71 -252
  43. package/lib/maps/MarkersLayerDesign.d.ts +4 -0
  44. package/lib/maps/MarkersLayerDesignerComponent.d.ts +20 -16
  45. package/lib/maps/MarkersLayerDesignerComponent.js +77 -23
  46. package/lib/maps/ServerMapDataSource.d.ts +0 -1
  47. package/lib/maps/ServerMapDataSource.js +0 -15
  48. package/lib/maps/SwitchableTileUrlLayer.d.ts +0 -2
  49. package/lib/maps/SwitchableTileUrlLayer.js +0 -9
  50. package/lib/maps/TileUrlLayer.d.ts +0 -1
  51. package/lib/maps/TileUrlLayer.js +0 -5
  52. package/lib/maps/VectorMapViewComponent.js +12 -1
  53. package/lib/maps/vectorMaps.d.ts +5 -6
  54. package/lib/maps/vectorMaps.js +13 -9
  55. package/lib/widgets/MapWidget.js +2 -1
  56. package/package.json +2 -2
  57. package/src/ColorComponent.tsx +2 -2
  58. package/src/TranslationsTabComponent.tsx +429 -0
  59. package/src/dashboards/DashboardComponent.tsx +1 -1
  60. package/src/dashboards/ServerDashboardDataSource.ts +0 -19
  61. package/src/dashboards/SettingsModalComponent.tsx +27 -383
  62. package/src/datagrids/DatagridComponent.tsx +6 -0
  63. package/src/datagrids/DatagridViewComponent.tsx +41 -5
  64. package/src/maps/BufferLayer.ts +16 -262
  65. package/src/maps/BufferLayerDesignerComponent.tsx +0 -6
  66. package/src/maps/ChoroplethLayer.ts +16 -393
  67. package/src/maps/ClusterLayer.ts +0 -274
  68. package/src/maps/DirectMapDataSource.ts +2 -49
  69. package/src/maps/GridLayer.ts +0 -224
  70. package/src/maps/Layer.ts +1 -35
  71. package/src/maps/MapComponent.tsx +448 -0
  72. package/src/maps/MapControlComponent.tsx +41 -0
  73. package/src/maps/MapDesign.ts +6 -0
  74. package/src/maps/MapDesignerComponent.tsx +18 -1
  75. package/src/maps/MapLayerDataSource.ts +0 -5
  76. package/src/maps/MapLayerViewDesignerComponent.ts +9 -2
  77. package/src/maps/MapLayersDesignerComponent.ts +4 -1
  78. package/src/maps/MapTranslationsTab.tsx +53 -0
  79. package/src/maps/MapUtils.ts +48 -0
  80. package/src/maps/MapViewComponent.tsx +2 -8
  81. package/src/maps/MarkersLayer.ts +79 -270
  82. package/src/maps/MarkersLayerDesign.ts +6 -0
  83. package/src/maps/MarkersLayerDesignerComponent.tsx +114 -38
  84. package/src/maps/ServerMapDataSource.ts +0 -19
  85. package/src/maps/SwitchableTileUrlLayer.tsx +0 -11
  86. package/src/maps/TileUrlLayer.tsx +0 -6
  87. package/src/maps/VectorMapViewComponent.tsx +13 -2
  88. package/src/maps/vectorMaps.tsx +12 -9
  89. package/src/widgets/MapWidget.tsx +2 -0
  90. package/src/maps/MapComponent.ts +0 -311
  91. package/src/maps/MapControlComponent.ts +0 -46
  92. 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;
@@ -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 = props.translate || ((input) => input);
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
@@ -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): import("maplibre-gl").StyleSpecification | null;
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): import("maplibre-gl").StyleSpecification | 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;
@@ -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
- preserveDrawingBuffer: printingModeEnabled
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, (err, image) => {
128
- if (image && !m.hasImage(mapSymbol.value)) {
129
- m.addImage(mapSymbol.value, image, { sdf: true });
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
  });
@@ -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
- 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 });
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.0",
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": "^3.3.1",
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",
@@ -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 + window.scrollY}px`,
55
- left: `${rect.left + window.scrollX}px`,
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
  */