@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
package/lib/ColorComponent.js
CHANGED
|
@@ -60,8 +60,8 @@ class ColorComponent extends react_1.default.Component {
|
|
|
60
60
|
const rect = this.colorWellRef.getBoundingClientRect();
|
|
61
61
|
const popupPosition = {
|
|
62
62
|
position: "fixed",
|
|
63
|
-
top: `${rect.bottom
|
|
64
|
-
left: `${rect.left
|
|
63
|
+
top: `${rect.bottom}px`,
|
|
64
|
+
left: `${rect.left}px`,
|
|
65
65
|
zIndex: 1070,
|
|
66
66
|
backgroundColor: "white",
|
|
67
67
|
border: "solid 1px #DDD",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface TranslationsTabComponentProps {
|
|
3
|
+
/** Base locale of the design (e.g. "en") */
|
|
4
|
+
locale: string;
|
|
5
|
+
/** Other locales the design is translated into */
|
|
6
|
+
otherLocales: string[];
|
|
7
|
+
/** Translation mappings per locale. Maps locale to { originalString: translatedString } */
|
|
8
|
+
translations: {
|
|
9
|
+
[locale: string]: {
|
|
10
|
+
[key: string]: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** All strings that need translation */
|
|
14
|
+
translatableStrings: string[];
|
|
15
|
+
/** Called when base locale changes */
|
|
16
|
+
onLocaleChange: (locale: string) => void;
|
|
17
|
+
/** Called when other locales change */
|
|
18
|
+
onOtherLocalesChange: (locales: string[]) => void;
|
|
19
|
+
/** Called when translations change */
|
|
20
|
+
onTranslationsChange: (translations: {
|
|
21
|
+
[locale: string]: {
|
|
22
|
+
[key: string]: string;
|
|
23
|
+
};
|
|
24
|
+
}) => void;
|
|
25
|
+
/** Custom filename for download. Defaults to "Translations.xlsx" */
|
|
26
|
+
downloadFilename?: string;
|
|
27
|
+
/** Custom description for base language section */
|
|
28
|
+
baseLanguageDescription?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reusable translations tab component for managing localization of designs
|
|
32
|
+
* (dashboards, maps, etc.) that have locale, otherLocales, and translations properties.
|
|
33
|
+
*/
|
|
34
|
+
export declare function TranslationsTabComponent(props: TranslationsTabComponentProps): React.JSX.Element;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.TranslationsTabComponent = TranslationsTabComponent;
|
|
30
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
31
|
+
const react_1 = __importStar(require("react"));
|
|
32
|
+
const languages_1 = require("./languages");
|
|
33
|
+
const react_select_1 = __importDefault(require("react-select"));
|
|
34
|
+
const file_saver_1 = __importDefault(require("file-saver"));
|
|
35
|
+
const localizeUtils = __importStar(require("ez-localize/lib/utils"));
|
|
36
|
+
const autotranslate_1 = require("./autotranslate");
|
|
37
|
+
const bootstrap_1 = require("@mwater/react-library/lib/bootstrap");
|
|
38
|
+
/**
|
|
39
|
+
* Reusable translations tab component for managing localization of designs
|
|
40
|
+
* (dashboards, maps, etc.) that have locale, otherLocales, and translations properties.
|
|
41
|
+
*/
|
|
42
|
+
function TranslationsTabComponent(props) {
|
|
43
|
+
const { locale, otherLocales, translations, translatableStrings, onLocaleChange, onOtherLocalesChange, onTranslationsChange, downloadFilename = "Translations.xlsx", baseLanguageDescription = T `This is the base language.` } = props;
|
|
44
|
+
const fileInputRef = (0, react_1.useRef)(null);
|
|
45
|
+
const localeOptions = (0, react_1.useMemo)(() => {
|
|
46
|
+
return lodash_1.default.sortBy(lodash_1.default.map(languages_1.languages, (language) => ({
|
|
47
|
+
value: language.code,
|
|
48
|
+
label: `${language.en} (${language.name})`
|
|
49
|
+
})), 'label');
|
|
50
|
+
}, []);
|
|
51
|
+
// Get available languages that aren't already selected
|
|
52
|
+
const availableLocaleOptions = (0, react_1.useMemo)(() => {
|
|
53
|
+
const selectedLocales = new Set([locale, ...otherLocales]);
|
|
54
|
+
return localeOptions.filter(opt => !selectedLocales.has(opt.value));
|
|
55
|
+
}, [localeOptions, locale, otherLocales]);
|
|
56
|
+
// Calculate percentage of strings translated for each locale
|
|
57
|
+
const translationPercentages = (0, react_1.useMemo)(() => {
|
|
58
|
+
const percentages = {};
|
|
59
|
+
const totalStrings = translatableStrings.length;
|
|
60
|
+
for (const loc of otherLocales) {
|
|
61
|
+
const translatedCount = translatableStrings.filter(str => translations?.[loc]?.[str] != null).length;
|
|
62
|
+
// Round down to nearest percent
|
|
63
|
+
percentages[loc] = (totalStrings > 0) ? Math.floor((translatedCount / totalStrings) * 100) : 0;
|
|
64
|
+
}
|
|
65
|
+
return percentages;
|
|
66
|
+
}, [translations, otherLocales, translatableStrings]);
|
|
67
|
+
const handleAddLocale = (selectedLocale) => {
|
|
68
|
+
const newOtherLocales = [...otherLocales, selectedLocale.value];
|
|
69
|
+
onOtherLocalesChange(newOtherLocales);
|
|
70
|
+
// Note: We don't initialize translations here because calling both callbacks
|
|
71
|
+
// in sequence would cause a race condition where the second overwrites the first.
|
|
72
|
+
// The code handles undefined translations via optional chaining.
|
|
73
|
+
};
|
|
74
|
+
const handleRemoveLocale = (localeToRemove) => {
|
|
75
|
+
const newOtherLocales = otherLocales.filter(loc => loc !== localeToRemove);
|
|
76
|
+
onOtherLocalesChange(newOtherLocales);
|
|
77
|
+
// Note: We don't remove translations here for the same reason as above.
|
|
78
|
+
// Orphaned translations are harmless and will be cleaned up on next save.
|
|
79
|
+
};
|
|
80
|
+
// Convert translations to LocalizedString format
|
|
81
|
+
const getLocalizedStrings = () => {
|
|
82
|
+
const localizedStrings = [];
|
|
83
|
+
for (const str of translatableStrings) {
|
|
84
|
+
const localizedString = { _base: locale };
|
|
85
|
+
localizedString[locale] = str;
|
|
86
|
+
// Only add translations for other locales if they exist
|
|
87
|
+
for (const otherLocale of otherLocales) {
|
|
88
|
+
if (translations?.[otherLocale]?.[str]) {
|
|
89
|
+
localizedString[otherLocale] = translations[otherLocale][str];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
localizedStrings.push(localizedString);
|
|
93
|
+
}
|
|
94
|
+
return localizedStrings;
|
|
95
|
+
};
|
|
96
|
+
// Convert LocalizedString format back to translations
|
|
97
|
+
const updateFromLocalizedStrings = (localizedStrings) => {
|
|
98
|
+
const newTranslations = {};
|
|
99
|
+
// Initialize translations object
|
|
100
|
+
for (const loc of otherLocales) {
|
|
101
|
+
newTranslations[loc] = {};
|
|
102
|
+
}
|
|
103
|
+
// Add all translations
|
|
104
|
+
for (const str of localizedStrings) {
|
|
105
|
+
for (const loc of otherLocales) {
|
|
106
|
+
if (str[loc]) {
|
|
107
|
+
newTranslations[loc][str[str._base]] = str[loc];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return newTranslations;
|
|
112
|
+
};
|
|
113
|
+
const handleDownload = () => {
|
|
114
|
+
// Get strings in LocalizedString format
|
|
115
|
+
const strings = getLocalizedStrings();
|
|
116
|
+
// Create xlsx base64 using all locales (primary + other)
|
|
117
|
+
const locales = [{ code: locale, name: locale }]
|
|
118
|
+
.concat(otherLocales.map(code => ({ code, name: code })));
|
|
119
|
+
const base64 = localizeUtils.exportXlsx(locales, strings);
|
|
120
|
+
// Download
|
|
121
|
+
file_saver_1.default.saveAs(b64toBlob(base64, "application/octet-stream"), downloadFilename);
|
|
122
|
+
};
|
|
123
|
+
const handleUpload = () => {
|
|
124
|
+
fileInputRef.current?.click();
|
|
125
|
+
};
|
|
126
|
+
const handleUploadChange = (evt) => {
|
|
127
|
+
const reader = new FileReader();
|
|
128
|
+
reader.onload = (file) => {
|
|
129
|
+
if (!file.target?.result) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const base64 = file.target.result.split(",")[1];
|
|
133
|
+
try {
|
|
134
|
+
// Create locales array for import
|
|
135
|
+
const locales = [{ code: locale, name: locale }]
|
|
136
|
+
.concat(otherLocales.map(code => ({ code, name: code })));
|
|
137
|
+
// Import updates
|
|
138
|
+
const updates = localizeUtils.importXlsx(locales, base64);
|
|
139
|
+
// If nothing localized
|
|
140
|
+
if (updates.length === 0) {
|
|
141
|
+
alert(T `No translation data found in file`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Convert back to translations format and update
|
|
145
|
+
const newTranslations = updateFromLocalizedStrings(updates);
|
|
146
|
+
onTranslationsChange(newTranslations);
|
|
147
|
+
alert(T `${updates.length} translations applied`);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error("Invalid xlsx file:", error);
|
|
151
|
+
alert(T `Invalid xlsx file`);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
if (evt.target.files?.[0]) {
|
|
155
|
+
reader.readAsDataURL(evt.target.files[0]);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const handleTranslationsChangeForLocale = (loc, newLocaleTranslations) => {
|
|
159
|
+
const newTranslations = { ...translations };
|
|
160
|
+
newTranslations[loc] = newLocaleTranslations;
|
|
161
|
+
onTranslationsChange(newTranslations);
|
|
162
|
+
};
|
|
163
|
+
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
164
|
+
react_1.default.createElement(bootstrap_1.FormGroup, { label: T `Base Language`, labelMuted: true, help: baseLanguageDescription },
|
|
165
|
+
react_1.default.createElement(react_select_1.default, { value: lodash_1.default.findWhere(localeOptions, { value: locale }) || null, options: localeOptions, onChange: (selected) => onLocaleChange(selected.value) })),
|
|
166
|
+
react_1.default.createElement(bootstrap_1.FormGroup, { label: T `Additional Languages`, labelMuted: true, help: T `Add languages to translate into` },
|
|
167
|
+
react_1.default.createElement("table", null,
|
|
168
|
+
react_1.default.createElement("tbody", null, otherLocales.map(loc => {
|
|
169
|
+
const localeOption = lodash_1.default.findWhere(localeOptions, { value: loc });
|
|
170
|
+
if (!localeOption) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return (react_1.default.createElement("tr", { key: loc },
|
|
174
|
+
react_1.default.createElement("td", { style: { paddingRight: 10 } }, localeOption.label),
|
|
175
|
+
react_1.default.createElement("td", { className: translationPercentages[loc] === 100 ? "text-success" : "text-warning", style: { textAlign: "right" } }, T `${translationPercentages[loc]}% translated`),
|
|
176
|
+
react_1.default.createElement("td", null, translationPercentages[loc] < 100 && (react_1.default.createElement(AutoTranslateLink, { locale: loc, baseLocale: locale, strings: translatableStrings, existingTranslations: translations?.[loc] || {}, onTranslationsChange: (newLocaleTranslations) => handleTranslationsChangeForLocale(loc, newLocaleTranslations) }))),
|
|
177
|
+
react_1.default.createElement("td", null,
|
|
178
|
+
react_1.default.createElement("button", { type: "button", className: "btn btn-sm btn-link", onClick: () => handleRemoveLocale(loc) },
|
|
179
|
+
react_1.default.createElement("i", { className: "fa fa-times" })))));
|
|
180
|
+
}))),
|
|
181
|
+
availableLocaleOptions.length > 0 && (react_1.default.createElement("div", { className: "mt-3" },
|
|
182
|
+
react_1.default.createElement(react_select_1.default, { value: null, options: availableLocaleOptions, onChange: handleAddLocale, placeholder: T `Add language...` })))),
|
|
183
|
+
otherLocales.length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
184
|
+
react_1.default.createElement(bootstrap_1.FormGroup, { label: T `Manage Translations`, labelMuted: true, help: T `Download and re-upload an Excel spreadsheet of text to translate:` },
|
|
185
|
+
react_1.default.createElement("p", null, T `Download and re-upload an Excel spreadsheet of text to translate:`),
|
|
186
|
+
react_1.default.createElement("div", null,
|
|
187
|
+
react_1.default.createElement("button", { type: "button", className: "btn btn-secondary", onClick: handleDownload },
|
|
188
|
+
react_1.default.createElement("i", { className: "fas fa-download me-2" }),
|
|
189
|
+
T `Download XLSX`)),
|
|
190
|
+
react_1.default.createElement("div", { className: "text-muted mt-2" }, 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.`),
|
|
191
|
+
react_1.default.createElement("br", null),
|
|
192
|
+
react_1.default.createElement("p", null, T `Once translation is complete, upload the file back using the button below:`),
|
|
193
|
+
react_1.default.createElement("div", null,
|
|
194
|
+
react_1.default.createElement("button", { type: "button", className: "btn btn-secondary", onClick: handleUpload },
|
|
195
|
+
react_1.default.createElement("i", { className: "fas fa-upload me-2" }),
|
|
196
|
+
T `Upload Translated XLSX`)),
|
|
197
|
+
react_1.default.createElement("input", { type: "file", ref: fileInputRef, style: { display: "none" }, onChange: handleUploadChange, accept: ".xlsx" }))))));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Button that auto-translates untranslated strings using the translation service
|
|
201
|
+
*/
|
|
202
|
+
function AutoTranslateLink(props) {
|
|
203
|
+
const { locale, baseLocale, strings, existingTranslations, onTranslationsChange } = props;
|
|
204
|
+
const [isTranslating, setIsTranslating] = (0, react_1.useState)(false);
|
|
205
|
+
const untranslatedStrings = (0, react_1.useMemo)(() => {
|
|
206
|
+
return strings.filter(str => !existingTranslations[str]);
|
|
207
|
+
}, [strings, existingTranslations]);
|
|
208
|
+
const handleClick = async () => {
|
|
209
|
+
if (isTranslating) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
setIsTranslating(true);
|
|
213
|
+
try {
|
|
214
|
+
const translatedStrings = await (0, autotranslate_1.translateStrings)(untranslatedStrings, baseLocale, locale);
|
|
215
|
+
const newTranslations = { ...existingTranslations };
|
|
216
|
+
for (let i = 0; i < untranslatedStrings.length; i++) {
|
|
217
|
+
newTranslations[untranslatedStrings[i]] = translatedStrings[i];
|
|
218
|
+
}
|
|
219
|
+
onTranslationsChange(newTranslations);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error("Error translating strings:", error);
|
|
223
|
+
alert(T `Error translating strings`);
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
setIsTranslating(false);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
if (untranslatedStrings.length === 0) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (!(0, autotranslate_1.canAutoTranslate)(locale)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
return (react_1.default.createElement("button", { type: "button", className: "btn btn-sm btn-link", onClick: handleClick, disabled: isTranslating, style: { marginLeft: 5, padding: "0 5px" } }, isTranslating ? (react_1.default.createElement("span", null,
|
|
236
|
+
react_1.default.createElement("i", { className: "fa fa-spinner fa-spin" }),
|
|
237
|
+
" ",
|
|
238
|
+
T `Translating...`)) : (T `Autotranslate`)));
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Helper function for base64 to blob conversion
|
|
242
|
+
*/
|
|
243
|
+
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
|
|
244
|
+
const byteCharacters = atob(b64Data);
|
|
245
|
+
const byteArrays = [];
|
|
246
|
+
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
247
|
+
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
248
|
+
const byteNumbers = new Array(slice.length);
|
|
249
|
+
for (let i = 0; i < slice.length; i++) {
|
|
250
|
+
byteNumbers[i] = slice.charCodeAt(i);
|
|
251
|
+
}
|
|
252
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
253
|
+
byteArrays.push(byteArray);
|
|
254
|
+
}
|
|
255
|
+
return new Blob(byteArrays, { type: contentType });
|
|
256
|
+
}
|
|
@@ -197,7 +197,7 @@ class DashboardComponent extends react_1.default.Component {
|
|
|
197
197
|
react_1.default.createElement("span", { className: "fal fa-globe" }),
|
|
198
198
|
" ",
|
|
199
199
|
this.state.locale),
|
|
200
|
-
react_1.default.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, [this.props.design.locale || "en", ...this.props.design.otherLocales].map(locale => react_1.default.createElement("li", { key: locale },
|
|
200
|
+
react_1.default.createElement("ul", { className: "dropdown-menu dropdown-menu-end" }, [...new Set([this.props.design.locale || "en", ...this.props.design.otherLocales])].map(locale => react_1.default.createElement("li", { key: locale },
|
|
201
201
|
react_1.default.createElement("a", { className: "dropdown-item", onClick: () => this.setState({ locale: locale }) }, __1.languages.find(l => l.code === locale)?.name || locale)))))
|
|
202
202
|
: undefined,
|
|
203
203
|
react_1.default.createElement("a", { key: "print", className: "btn btn-link btn-sm", onClick: this.handlePrint },
|
|
@@ -66,7 +66,6 @@ declare class ServerWidgetLayerDataSource implements MapLayerDataSource {
|
|
|
66
66
|
options: ServerWidgetLayerDataSourceOptions;
|
|
67
67
|
constructor(options: ServerWidgetLayerDataSourceOptions);
|
|
68
68
|
getTileUrl(design: any, filters: JsonQLFilter[]): any;
|
|
69
|
-
getUtfGridUrl(design: any, filters: JsonQLFilter[]): string | null;
|
|
70
69
|
/** Get the url for vector tile source with an expiry time. Only for layers of type "VectorTile"
|
|
71
70
|
* @param createdAfter ISO 8601 timestamp requiring that tile source on server is created after specified datetime
|
|
72
71
|
*/
|
|
@@ -231,21 +231,6 @@ class ServerWidgetLayerDataSource {
|
|
|
231
231
|
}
|
|
232
232
|
return this.createUrl(filters, "png");
|
|
233
233
|
}
|
|
234
|
-
// Get the url for the interactivity tiles with the specified filters applied
|
|
235
|
-
// Called with (design, filters) where design is the layer design and filters are filters to apply. Returns URL
|
|
236
|
-
getUtfGridUrl(design, filters) {
|
|
237
|
-
// Handle special cases
|
|
238
|
-
if (this.options.layerView.type === "MWaterServer") {
|
|
239
|
-
return this.createLegacyUrl(this.options.layerView.design, "grid.json", filters);
|
|
240
|
-
}
|
|
241
|
-
// Create layer
|
|
242
|
-
const layer = LayerFactory_1.default.createLayer(this.options.layerView.type);
|
|
243
|
-
// If layer has tiles url directly available
|
|
244
|
-
if (layer.getLayerDefinitionType() === "TileUrl") {
|
|
245
|
-
return layer.getUtfGridUrl(this.options.layerView.design, filters);
|
|
246
|
-
}
|
|
247
|
-
return this.createUrl(filters, "grid.json");
|
|
248
|
-
}
|
|
249
234
|
/** Get the url for vector tile source with an expiry time. Only for layers of type "VectorTile"
|
|
250
235
|
* @param createdAfter ISO 8601 timestamp requiring that tile source on server is created after specified datetime
|
|
251
236
|
*/
|
|
@@ -26,11 +26,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
26
26
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
const lodash_1 = __importDefault(require("lodash"));
|
|
30
29
|
const react_1 = __importStar(require("react"));
|
|
31
|
-
const languages_1 = require("../languages");
|
|
32
30
|
const ui = __importStar(require("@mwater/react-library/lib/bootstrap"));
|
|
33
|
-
const react_select_1 = __importDefault(require("react-select"));
|
|
34
31
|
const DashboardUtils = __importStar(require("./DashboardUtils"));
|
|
35
32
|
const ActionCancelModalComponent_1 = __importDefault(require("@mwater/react-library/lib/ActionCancelModalComponent"));
|
|
36
33
|
const QuickfiltersDesignComponent_1 = __importDefault(require("../quickfilter/QuickfiltersDesignComponent"));
|
|
@@ -38,9 +35,7 @@ const FiltersDesignerComponent_1 = __importDefault(require("../FiltersDesignerCo
|
|
|
38
35
|
const MWaterContextComponent_1 = require("../MWaterContextComponent");
|
|
39
36
|
const immer_1 = __importDefault(require("immer"));
|
|
40
37
|
const TabbedComponent_1 = __importDefault(require("@mwater/react-library/lib/TabbedComponent"));
|
|
41
|
-
const
|
|
42
|
-
const localizeUtils = __importStar(require("ez-localize/lib/utils"));
|
|
43
|
-
const autotranslate_1 = require("../autotranslate");
|
|
38
|
+
const TranslationsTabComponent_1 = require("../TranslationsTabComponent");
|
|
44
39
|
/** Popup with settings for dashboard */
|
|
45
40
|
class SettingsModalComponent extends react_1.default.Component {
|
|
46
41
|
constructor(props) {
|
|
@@ -127,233 +122,14 @@ function FiltersTab({ design, onDesignChange, schema, dataSource }) {
|
|
|
127
122
|
draft.implicitFiltersEnabled = value;
|
|
128
123
|
})) }, T `Enable Implicit Filtering (leave unchecked for new dashboards)`)))));
|
|
129
124
|
}
|
|
125
|
+
/** Wrapper around TranslationsTabComponent for dashboard-specific usage */
|
|
130
126
|
function LanguageTab({ design, onDesignChange, schema }) {
|
|
131
|
-
const fileInputRef = (0, react_1.useRef)(null);
|
|
132
|
-
const locale = design.locale || "en";
|
|
133
|
-
const localeOptions = (0, react_1.useMemo)(() => {
|
|
134
|
-
return lodash_1.default.sortBy(lodash_1.default.map(languages_1.languages, (language) => ({
|
|
135
|
-
value: language.code,
|
|
136
|
-
label: `${language.en} (${language.name})`
|
|
137
|
-
})), 'label');
|
|
138
|
-
}, [languages_1.languages]);
|
|
139
|
-
// Get available languages that aren't already selected
|
|
140
|
-
const availableLocaleOptions = (0, react_1.useMemo)(() => {
|
|
141
|
-
const selectedLocales = new Set([design.locale, ...(design.otherLocales || [])]);
|
|
142
|
-
return localeOptions.filter(opt => !selectedLocales.has(opt.value));
|
|
143
|
-
}, [localeOptions, design.locale, design.otherLocales]);
|
|
144
127
|
const translatableStrings = (0, react_1.useMemo)(() => DashboardUtils.getTranslatableStringsFromDashboard(design, schema), [design, schema]);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
percentages[locale] = (totalStrings > 0) ? Math.floor((translatedCount / totalStrings) * 100) : 0;
|
|
153
|
-
}
|
|
154
|
-
return percentages;
|
|
155
|
-
}, [design.translations, design.otherLocales, translatableStrings]);
|
|
156
|
-
const handleAddLocale = (locale) => {
|
|
157
|
-
onDesignChange((0, immer_1.default)(design, draft => {
|
|
158
|
-
draft.otherLocales = [...(draft.otherLocales || []), locale.value];
|
|
159
|
-
// Initialize empty translations object if needed
|
|
160
|
-
if (!draft.translations) {
|
|
161
|
-
draft.translations = {};
|
|
162
|
-
}
|
|
163
|
-
if (!draft.translations[locale.value]) {
|
|
164
|
-
draft.translations[locale.value] = {};
|
|
165
|
-
}
|
|
166
|
-
}));
|
|
167
|
-
};
|
|
168
|
-
const handleRemoveLocale = (localeToRemove) => {
|
|
169
|
-
onDesignChange((0, immer_1.default)(design, draft => {
|
|
170
|
-
draft.otherLocales = (draft.otherLocales || []).filter(locale => locale !== localeToRemove);
|
|
171
|
-
// Remove translations for this locale
|
|
172
|
-
if (draft.translations) {
|
|
173
|
-
delete draft.translations[localeToRemove];
|
|
174
|
-
}
|
|
175
|
-
}));
|
|
176
|
-
};
|
|
177
|
-
// Convert dashboard translations to LocalizedString format
|
|
178
|
-
const getLocalizedStrings = () => {
|
|
179
|
-
// For each string, create a localized string
|
|
180
|
-
const localizedStrings = [];
|
|
181
|
-
for (const str of translatableStrings) {
|
|
182
|
-
const localizedString = { _base: locale };
|
|
183
|
-
localizedString[locale] = str;
|
|
184
|
-
// Only add translations for other locales if they exist
|
|
185
|
-
for (const otherLocale of design.otherLocales || []) {
|
|
186
|
-
if (design.translations?.[otherLocale]?.[str]) {
|
|
187
|
-
localizedString[otherLocale] = design.translations?.[otherLocale]?.[str];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
localizedStrings.push(localizedString);
|
|
191
|
-
}
|
|
192
|
-
return localizedStrings;
|
|
193
|
-
};
|
|
194
|
-
// Convert LocalizedString format back to dashboard translations
|
|
195
|
-
const updateFromLocalizedStrings = (localizedStrings) => {
|
|
196
|
-
const newTranslations = {};
|
|
197
|
-
// Get all locales except base
|
|
198
|
-
const locales = design.otherLocales || [];
|
|
199
|
-
// Initialize translations object
|
|
200
|
-
for (const locale of locales) {
|
|
201
|
-
newTranslations[locale] = {};
|
|
202
|
-
}
|
|
203
|
-
// Add all translations
|
|
204
|
-
for (const str of localizedStrings) {
|
|
205
|
-
for (const locale of locales) {
|
|
206
|
-
if (str[locale]) {
|
|
207
|
-
newTranslations[locale][str[str._base]] = str[locale];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
return newTranslations;
|
|
212
|
-
};
|
|
213
|
-
const handleDownload = () => {
|
|
214
|
-
// Get strings in LocalizedString format
|
|
215
|
-
const strings = getLocalizedStrings();
|
|
216
|
-
// Create xlsx base64 using all locales (primary + other)
|
|
217
|
-
const locales = [{ code: locale, name: locale }]
|
|
218
|
-
.concat((design.otherLocales || []).map(code => ({ code, name: code })));
|
|
219
|
-
const base64 = localizeUtils.exportXlsx(locales, strings);
|
|
220
|
-
// Download
|
|
221
|
-
file_saver_1.default.saveAs(b64toBlob(base64, "application/octet-stream"), "Dashboard Translations.xlsx");
|
|
222
|
-
};
|
|
223
|
-
const handleUpload = () => {
|
|
224
|
-
fileInputRef.current?.click();
|
|
225
|
-
};
|
|
226
|
-
const handleUploadChange = (evt) => {
|
|
227
|
-
const reader = new FileReader();
|
|
228
|
-
reader.onload = (file) => {
|
|
229
|
-
if (!file.target?.result) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const base64 = file.target.result.split(",")[1];
|
|
233
|
-
try {
|
|
234
|
-
// Create locales array for import
|
|
235
|
-
const locales = [{ code: locale, name: locale }]
|
|
236
|
-
.concat((design.otherLocales || []).map(code => ({ code, name: code })));
|
|
237
|
-
// Import updates
|
|
238
|
-
const updates = localizeUtils.importXlsx(locales, base64);
|
|
239
|
-
// If nothing localized
|
|
240
|
-
if (updates.length === 0) {
|
|
241
|
-
alert(T `No translation data found in file`);
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// Convert back to dashboard format and update
|
|
245
|
-
const newTranslations = updateFromLocalizedStrings(updates);
|
|
246
|
-
onDesignChange((0, immer_1.default)(design, draft => {
|
|
247
|
-
draft.translations = newTranslations;
|
|
248
|
-
}));
|
|
249
|
-
alert(T `${updates.length} translations applied`);
|
|
250
|
-
}
|
|
251
|
-
catch (error) {
|
|
252
|
-
console.error("Invalid xlsx file:", error);
|
|
253
|
-
alert(T `Invalid xlsx file`);
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
if (evt.target.files?.[0]) {
|
|
257
|
-
reader.readAsDataURL(evt.target.files[0]);
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
return (react_1.default.createElement(react_1.default.Fragment, null,
|
|
261
|
-
react_1.default.createElement("h4", null, T `Base Language`),
|
|
262
|
-
react_1.default.createElement("div", { className: "mb-2" }, T `This is the language that the dashboard is written in.`),
|
|
263
|
-
react_1.default.createElement(react_select_1.default, { value: lodash_1.default.findWhere(localeOptions, { value: design.locale || "en" }) || null, options: localeOptions, onChange: (locale) => onDesignChange((0, immer_1.default)(design, draft => {
|
|
264
|
-
draft.locale = locale.value;
|
|
265
|
-
})) }),
|
|
266
|
-
react_1.default.createElement("h4", { className: "mt-4" }, T `Additional Languages`),
|
|
267
|
-
react_1.default.createElement("div", { className: "mb-2" }, T `Add languages that this dashboard will be translated into`),
|
|
268
|
-
react_1.default.createElement("table", null,
|
|
269
|
-
react_1.default.createElement("tbody", null, (design.otherLocales || []).map(locale => {
|
|
270
|
-
const localeOption = lodash_1.default.findWhere(localeOptions, { value: locale });
|
|
271
|
-
if (!localeOption) {
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
return (react_1.default.createElement("tr", { key: locale },
|
|
275
|
-
react_1.default.createElement("td", { style: { paddingRight: 10 } }, localeOption.label),
|
|
276
|
-
react_1.default.createElement("td", { className: translationPercentages[locale] === 100 ? "text-success" : "text-warning", style: { textAlign: "right" } }, T `${translationPercentages[locale]}% translated`),
|
|
277
|
-
react_1.default.createElement("td", null, translationPercentages[locale] < 100 && (react_1.default.createElement(AutoTranslateLink, { locale: locale, baseLocale: design.locale || "en", strings: translatableStrings, existingTranslations: design.translations?.[locale] || {}, onTranslationsChange: (newTranslations) => {
|
|
278
|
-
onDesignChange((0, immer_1.default)(design, draft => {
|
|
279
|
-
if (!draft.translations) {
|
|
280
|
-
draft.translations = {};
|
|
281
|
-
}
|
|
282
|
-
draft.translations[locale] = newTranslations;
|
|
283
|
-
}));
|
|
284
|
-
} }))),
|
|
285
|
-
react_1.default.createElement("td", null,
|
|
286
|
-
react_1.default.createElement("button", { type: "button", className: "btn btn-sm btn-link", onClick: () => handleRemoveLocale(locale) },
|
|
287
|
-
react_1.default.createElement("i", { className: "fa fa-times" })))));
|
|
288
|
-
}))),
|
|
289
|
-
availableLocaleOptions.length > 0 && (react_1.default.createElement("div", { className: "mt-3" },
|
|
290
|
-
react_1.default.createElement(react_select_1.default, { value: null, options: availableLocaleOptions, onChange: handleAddLocale, placeholder: T `Add language...` }))),
|
|
291
|
-
(design.otherLocales || []).length > 0 && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
292
|
-
react_1.default.createElement("h4", { className: "mt-4" }, T `Manage Translations`),
|
|
293
|
-
react_1.default.createElement("p", null, T `Download and re-upload an Excel spreadsheet of text to translate:`),
|
|
294
|
-
react_1.default.createElement("div", null,
|
|
295
|
-
react_1.default.createElement("button", { type: "button", className: "btn btn-secondary", onClick: handleDownload },
|
|
296
|
-
react_1.default.createElement("i", { className: "fas fa-download me-2" }),
|
|
297
|
-
T `Download XLSX`)),
|
|
298
|
-
react_1.default.createElement("div", { className: "text-muted mt-2" }, 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.`),
|
|
299
|
-
react_1.default.createElement("br", null),
|
|
300
|
-
react_1.default.createElement("p", null, T `Once translation is complete, upload the file back using the button below:`),
|
|
301
|
-
react_1.default.createElement("div", null,
|
|
302
|
-
react_1.default.createElement("button", { type: "button", className: "btn btn-secondary", onClick: handleUpload },
|
|
303
|
-
react_1.default.createElement("i", { className: "fas fa-upload me-2" }),
|
|
304
|
-
T `Upload Translated XLSX`)),
|
|
305
|
-
react_1.default.createElement("input", { type: "file", ref: fileInputRef, style: { display: "none" }, onChange: handleUploadChange, accept: ".xlsx" })))));
|
|
306
|
-
}
|
|
307
|
-
/** Helper function for base64 to blob conversion */
|
|
308
|
-
function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
|
|
309
|
-
const byteCharacters = atob(b64Data);
|
|
310
|
-
const byteArrays = [];
|
|
311
|
-
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
|
|
312
|
-
const slice = byteCharacters.slice(offset, offset + sliceSize);
|
|
313
|
-
const byteNumbers = new Array(slice.length);
|
|
314
|
-
for (let i = 0; i < slice.length; i++) {
|
|
315
|
-
byteNumbers[i] = slice.charCodeAt(i);
|
|
316
|
-
}
|
|
317
|
-
const byteArray = new Uint8Array(byteNumbers);
|
|
318
|
-
byteArrays.push(byteArray);
|
|
319
|
-
}
|
|
320
|
-
return new Blob(byteArrays, { type: contentType });
|
|
321
|
-
}
|
|
322
|
-
function AutoTranslateLink(props) {
|
|
323
|
-
const { locale, baseLocale, strings, existingTranslations, onTranslationsChange } = props;
|
|
324
|
-
const [isTranslating, setIsTranslating] = (0, react_1.useState)(false);
|
|
325
|
-
const untranslatedStrings = (0, react_1.useMemo)(() => {
|
|
326
|
-
return strings.filter(str => !existingTranslations[str]);
|
|
327
|
-
}, [strings, existingTranslations]);
|
|
328
|
-
const handleClick = async () => {
|
|
329
|
-
if (isTranslating) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
setIsTranslating(true);
|
|
333
|
-
try {
|
|
334
|
-
const translatedStrings = await (0, autotranslate_1.translateStrings)(untranslatedStrings, baseLocale, locale);
|
|
335
|
-
const newTranslations = { ...existingTranslations };
|
|
336
|
-
for (let i = 0; i < untranslatedStrings.length; i++) {
|
|
337
|
-
newTranslations[untranslatedStrings[i]] = translatedStrings[i];
|
|
338
|
-
}
|
|
339
|
-
onTranslationsChange(newTranslations);
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
console.error("Error translating strings:", error);
|
|
343
|
-
alert(T `Error translating strings`);
|
|
344
|
-
}
|
|
345
|
-
finally {
|
|
346
|
-
setIsTranslating(false);
|
|
347
|
-
}
|
|
348
|
-
};
|
|
349
|
-
if (untranslatedStrings.length === 0) {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
if (!(0, autotranslate_1.canAutoTranslate)(locale)) {
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
return (react_1.default.createElement("button", { type: "button", className: "btn btn-sm btn-link", onClick: handleClick, disabled: isTranslating, style: { marginLeft: 5, padding: "0 5px" } }, isTranslating ? (react_1.default.createElement("span", null,
|
|
356
|
-
react_1.default.createElement("i", { className: "fa fa-spinner fa-spin" }),
|
|
357
|
-
" ",
|
|
358
|
-
T `Translating...`)) : (T `Autotranslate`)));
|
|
128
|
+
return (react_1.default.createElement(TranslationsTabComponent_1.TranslationsTabComponent, { locale: design.locale || "en", otherLocales: design.otherLocales || [], translations: design.translations || {}, translatableStrings: translatableStrings, onLocaleChange: (locale) => onDesignChange((0, immer_1.default)(design, draft => {
|
|
129
|
+
draft.locale = locale;
|
|
130
|
+
})), onOtherLocalesChange: (otherLocales) => onDesignChange((0, immer_1.default)(design, draft => {
|
|
131
|
+
draft.otherLocales = otherLocales;
|
|
132
|
+
})), onTranslationsChange: (translations) => onDesignChange((0, immer_1.default)(design, draft => {
|
|
133
|
+
draft.translations = translations;
|
|
134
|
+
})), downloadFilename: "Dashboard Translations.xlsx", baseLanguageDescription: T `This is the language that the dashboard is written in.` }));
|
|
359
135
|
}
|
|
@@ -85,6 +85,11 @@ exports.default = (0, react_1.forwardRef)(function DatagridComponent(props, ref)
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
let activeFilters = filters || [];
|
|
88
|
+
// Clean before counting
|
|
89
|
+
const cleanedDesign = new DatagridUtils_1.default(schema).cleanDesign(design);
|
|
90
|
+
if (new DatagridUtils_1.default(schema).validateDesign(cleanedDesign)) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
88
93
|
// Compile quickfilters
|
|
89
94
|
activeFilters = activeFilters.concat(getQuickfilterFilters());
|
|
90
95
|
datagridDataSource.countRows(design, activeFilters, (error, count) => {
|