Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chart Title Localization #9

Merged
merged 6 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,30 +214,53 @@ For ease of development, this package also exports [a helper function `buildQuer

### `interface Translation`

An object whose keys are message keys, and its values the localized string to show in the interface.
An object whose keys are message keys, and its values the localized string to show in the interface. Here is an example of a `Translation` object and how it should be used.

```ts
interface Translation {
const translation: Translation = {
/* These are actions shown in buttons in the UI */
"action_close": string;
"action_enlarge": string;
"action_retry": string;
"action_fileissue": string;

action_close: "Close",
action_download: "Download {{format}}",
action_enlarge: "Enlarge",
action_fileissue: "File an issue",
action_retry: "Retry",
aggregators: {
avg: "Average",
max: "Max",
min: "Min"
},

/* These labels are shown in the charts tooltip */
"chart_labels": {
"ci": string;
"collection": string;
"moe": string;
"source": string;
};
chart_labels: {
ci: "Confidence Interval",
moe: "Margin of Error",
source: "Source",
collection: "Collection"
},

/* These labels are shown in the suggested error message when filing a new issue */
"error": {
"detail": string;
"message": string;
"title": string;
};
error: {
detail: "",
message: "Error details: \"{{message}}\".",
title: "Title: "
},

/* Message for the default NonIdealState when no charts are valid for queries */
nonidealstate_msg: "No results",

/* For listing words */
sentence_connectors: {
and: "and"
},

/* Sentence fragments for dynamically constructing chart titles (see example for use)*/
title: {
of_selected_cut_members: "of Selected {{members}} Members",
top_drilldowns: "for Top {{drilldowns}}",
by_drilldowns: "by {{drilldowns}}",
over_time: "Over Time",
measure_and_modifier: "{{modifier}} {{measure}}"
}
}
```

Expand Down
17 changes: 14 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ declare namespace VizBldr {
"action_enlarge": string;
"action_retry": string;
"action_fileissue": string;
"aggregators": {
"avg": string,
"max": string,
"min": string
};
"chart_labels": {
"ci": string;
"collection": string;
Expand All @@ -98,11 +103,17 @@ declare namespace VizBldr {
"message": string;
"title": string;
};
"nonidealstate_msg"?: string;
"sentence_connectors": {
"all_words": string;
"two_words": string;
"last_word": string;
"and": string;
};
"title": {
"of_selected_cut_members": string;
"top_drilldowns": string;
"by_drilldowns": string;
"over_time": string;
"measure_and_modifier": string;
}
}

namespace Struct {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"dependencies": {
"@babel/runtime-corejs3": "^7.0.0",
"@datawheel/olap-client": "^2.0.0-beta.3",
"@datawheel/use-translation": "^0.1.0",
"@datawheel/use-translation": "^0.1.3",
"classnames": "^2.0.0",
"d3plus-common": "^1.0.0",
"d3plus-export": "^1.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Vizbuilder.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const Vizbuilder = props => {
/>
);

// return NonIdealState comp if the list of available chart types is empty
return chartCards.length > 0 ? chartCards : props.nonIdealState || <NonIdealState/>;
// return NonIdealState comp if the list of available chart types is empty
return chartCards.length > 0 ? chartCards : props.nonIdealState || <NonIdealState/>;
}, [currentChart, currentPeriod, charts, props.showConfidenceInt]);

useEffect(() => {
Expand Down
2 changes: 1 addition & 1 deletion src/toolbox/chartConfigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function createChartConfig(chart, uiParams) {
config.zoom = chartType === "geomap" && isSingleChart;

if (config.title === undefined) {
config.title = chartTitleGenerator(chart, uiParams);
config.title = chartTitleGenerator(chart, uiParams.translate);
}

assign(config, uiParams.measureConfig[measureName] || {});
Expand Down
59 changes: 23 additions & 36 deletions src/toolbox/title.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
import { AggregatorType } from "@datawheel/olap-client";
import { findTimeRange } from "./find";
import { getColumnId } from "./strings";

/**
* Human-readable names of special aggregator types.
* Needed to make certain types of measures more explicit about their measure's derivation.
*
* If an aggregation type needs clarification beyond measure name, insert it here.
*/
const SPECIAL_AGGREGATOR_QUALIFIERS = {
[AggregatorType.AVG]: "Average",
[AggregatorType.MAX]: "Max",
[AggregatorType.MIN]: "Min"
};
import {findTimeRange} from "./find";
import {getColumnId} from "./strings";

/**
* Returns a common title string from a list of parameters.
Expand All @@ -35,22 +22,27 @@ const SPECIAL_AGGREGATOR_QUALIFIERS = {
* _MEASURE_NAME of Selected CUT_LEVEL_NAME Members by DRILLDOWN_1_
*
* @param {VizBldr.Struct.Chart} chart
* @param {object} options
* @param {Function} translate
*/
export function chartTitleGenerator(chart, options) {
export function chartTitleGenerator(chart, translate) {

const {dg, measureSet} = chart;
const {members} = dg;

let title;

// FIRST, add measure (with appropriate qualifiers) to the start of the title
title = `${getAggregationTypeQualifier(measureSet.measure.aggregatorType)}${measureSet.measure.name}`;
title = translate("title.measure_and_modifier", {
modifier: getAggregationTypeQualifier(measureSet.measure.aggregatorType, translate),
measure: measureSet.measure.name
}).trim(); // remove starting or trailing whitespaces

/** Set of cut level names, to be filtered to include only levels not accounted for in drilldowns */
const allCutNames = new Set(dg.cuts.keys());

/** List of Level names to include in CUT clause */
const cutLabels = [];

/** Labels of drilldowns with only a single member */
const singleMemberDrilldownLabels = [];

Expand All @@ -77,16 +69,14 @@ export function chartTitleGenerator(chart, options) {
cutLabels.unshift(...allCutNames.values());

// add labels for cut levels not included in Chart.levels
if (cutLabels.length > 0) title += ` of Selected ${arrayToSentence(cutLabels)} Members`;
if (cutLabels.length > 0) title += ` ${translate("title.of_selected_cut_members", {members: arrayToSentence(cutLabels, translate)})}`;

// add labels for levels with single member
if (singleMemberDrilldownLabels.length > 0) title += ` (${singleMemberDrilldownLabels.join(", ")})`;

// add levels with multiple members to titles
if (drilldownNames.length > 0) {
title += chart.isTopTen
? ` for top ${arrayToSentence(drilldownNames)}`
: ` by ${arrayToSentence(drilldownNames)}`;
title += ` ${translate(`title.${chart.isTopTen ? "top" : "by"}_drilldowns`, {drilldowns: arrayToSentence(drilldownNames, translate)})}`;
}

let titleFn = null;
Expand All @@ -105,46 +95,43 @@ export function chartTitleGenerator(chart, options) {
titleFn = data => {
const {minTime, maxTime} = findTimeRange(data, timeLevelId, timeLevelName);
return `${title} (${timeLevelName}: ${periodToString(minTime, maxTime !== minTime && maxTime)})`;
}
};
}
// else, if time is shown on one axis, say "Over Time"
else title += ` Over Time`;
else title += ` ${translate("title.over_time")}`;
}

return titleFn || title;
}

/**
* @param {string[]} strings
* @param {Record<string, string>} [options]
* @param {Function} translate
* @returns {string}
*/
function arrayToSentence(strings, options = {}) {
const allWords = options.all_words || ", ";
const twoWords = options.two_words || " and ";
const lastWord = options.last_word || ", and ";
function arrayToSentence(strings, translate) {
strings = strings.filter(Boolean);

if (strings.length === 2) {
return strings.join(twoWords);
return strings.join(` ${translate("sentence_connectors.and")} `);
}
if (strings.length > 1) {
const bulk = strings.slice();
const last = bulk.pop();
return [bulk.join(allWords), last].join(lastWord);
return [bulk.join(", "), last].join(` ${translate("sentence_connectors.and")} `);
}
return strings.join("");
}

/**
* Returns a string giving a few qualifying words (if necessary) to add to a measure in the case
* that a measure does not have a self-evident aggregation method (like sum)
* @param {import("@datawheel/olap-client").AggregatorType} aggregationType -
* @param {import("@datawheel/olap-client").AggregatorType} aggregationType
* @param {Function} translate - translation function
* @returns
*/
function getAggregationTypeQualifier(aggregationType) {
const qualifier = SPECIAL_AGGREGATOR_QUALIFIERS[aggregationType];
return qualifier ? `${qualifier} ` : "";
function getAggregationTypeQualifier(aggregationType, translate) {
const qualifier = aggregationType && typeof aggregationType === "string" && translate(`aggregators.${aggregationType.toLowerCase()}`);
return qualifier && !qualifier.startsWith("aggregators.") ? qualifier : "";
}

/**
Expand Down
20 changes: 15 additions & 5 deletions src/toolbox/useTranslation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ import {translationFactory} from "@datawheel/use-translation";
/** @type {VizBldr.Translation} */
const LOCALE_EN = {
action_close: "Close",
action_download: "Download {format}",
action_download: "Download {{format}}",
action_enlarge: "Enlarge",
action_fileissue: "File an issue",
action_retry: "Retry",
aggregators: {
avg: "Average",
max: "Max",
min: "Min"
},
chart_labels: {
ci: "Confidence Interval",
moe: "Margin of Error",
Expand All @@ -15,14 +20,19 @@ const LOCALE_EN = {
},
error: {
detail: "",
message: "Error details: \"{message}\".",
message: "Error details: \"{{message}}\".",
title: "Title: "
},
nonidealstate_msg: "No results",
sentence_connectors: {
all_words: ", ",
two_words: " and ",
last_word: ", and "
and: "and"
},
title: {
of_selected_cut_members: "of Selected {{members}} Members",
top_drilldowns: "for Top {{drilldowns}}",
by_drilldowns: "by {{drilldowns}}",
over_time: "Over Time",
measure_and_modifier: "{{modifier}} {{measure}}"
}
};

Expand Down