mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
Make a good part of the app localizable and add French translations (#325)
Co-authored-by: Yohan Boniface <yohanboniface@free.fr>
This commit is contained in:
@@ -19,6 +19,7 @@ import {ActionSummary, asTabularDiffs, defunctTableName, getAffectedTables,
|
||||
LabelDelta} from 'app/common/ActionSummary';
|
||||
import {CellDelta} from 'app/common/TabularDiff';
|
||||
import {IDomComponent} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -46,6 +47,8 @@ const state = {
|
||||
DEFAULT: 'default'
|
||||
};
|
||||
|
||||
const t = makeT('components.ActionLog');
|
||||
|
||||
export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
|
||||
private _displayStack: KoArray<ActionGroupWithState>;
|
||||
@@ -224,7 +227,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
}
|
||||
|
||||
private _buildLogDom() {
|
||||
this._loadActionSummaries().catch((error) => gristNotify(`Action Log failed to load`));
|
||||
this._loadActionSummaries().catch((error) => gristNotify(t("ActionLogFailed")));
|
||||
return dom('div.action_log',
|
||||
dom('div.preference_item',
|
||||
koForm.checkbox(this._showAllTables,
|
||||
@@ -392,7 +395,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
const newName = tableRename[1];
|
||||
if (!newName) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`Table ${tableId} was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t('TableRemovedInAction', {tableId:tableId, actionNum: action.actionNum}));
|
||||
return;
|
||||
}
|
||||
tableId = newName;
|
||||
@@ -403,7 +406,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
// Check is this row was removed - if so there's no reason to go on.
|
||||
if (td.removeRows.indexOf(rowId) >= 0) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`This row was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t("RowRemovedInAction", {actionNum}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -413,7 +416,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
const newName = columnRename[1];
|
||||
if (!newName) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`Column ${colId} was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t("ColumnRemovedInAction", {colId, actionNum: action.actionNum}));
|
||||
return;
|
||||
}
|
||||
colId = newName;
|
||||
|
||||
@@ -38,6 +38,7 @@ import sum = require('lodash/sum');
|
||||
import union = require('lodash/union');
|
||||
import type {Annotations, Config, Datum, ErrorBar, Layout, LayoutAxis, Margin,
|
||||
PlotData as PlotlyPlotData} from 'plotly.js';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
|
||||
let Plotly: PlotlyType;
|
||||
@@ -49,6 +50,8 @@ const DONUT_DEFAULT_TEXT_SIZE = 24;
|
||||
|
||||
const testId = makeTestId('test-chart-');
|
||||
|
||||
const t = makeT('components.ChartView');
|
||||
|
||||
function isPieLike(chartType: string) {
|
||||
return ['pie', 'donut'].includes(chartType);
|
||||
}
|
||||
@@ -652,8 +655,8 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
testId('error-bars'),
|
||||
),
|
||||
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
|
||||
value === 'symmetric' ? cssRowHelp('Each Y series is followed by a series for the length of error bars.') :
|
||||
value === 'separate' ? cssRowHelp('Each Y series is followed by two series, for top and bottom error bars.') :
|
||||
value === 'symmetric' ? cssRowHelp(t('EachYFollowedByOne')) :
|
||||
value === 'separate' ? cssRowHelp(t('EachYFollowedByTwo')) :
|
||||
null
|
||||
),
|
||||
]),
|
||||
@@ -666,7 +669,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
select(this._groupDataColId, this._groupDataOptions),
|
||||
testId('group-by-column'),
|
||||
),
|
||||
cssHintRow('Create separate series for each value of the selected column.'),
|
||||
cssHintRow(t('CreateSeparateSeries')),
|
||||
]),
|
||||
|
||||
// TODO: user should select x axis before widget reach page
|
||||
@@ -674,7 +677,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
cssRow(
|
||||
select(
|
||||
this._xAxis, this._columnsOptions,
|
||||
{ defaultLabel: 'Pick a column' }
|
||||
{ defaultLabel: t('PickColumn') }
|
||||
),
|
||||
testId('x-axis'),
|
||||
),
|
||||
@@ -770,7 +773,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
private async _setGroupDataColumn(colId: string) {
|
||||
const viewFields = this._section.viewFields.peek().peek();
|
||||
|
||||
await this._gristDoc.docData.bundleActions('selected new group data columnd', async () => {
|
||||
await this._gristDoc.docData.bundleActions(t('SelectedNewGroupDataColumns'), async () => {
|
||||
this._freezeXAxis.set(true);
|
||||
this._freezeYAxis.set(true);
|
||||
try {
|
||||
@@ -869,7 +872,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
private async _setAggregation(val: boolean) {
|
||||
try {
|
||||
this._freezeXAxis.set(true);
|
||||
await this._gristDoc.docData.bundleActions(`Toggle chart aggregation`, async () => {
|
||||
await this._gristDoc.docData.bundleActions(t("ToggleChartAggregation"), async () => {
|
||||
if (val) {
|
||||
await this._doAggregation();
|
||||
} else {
|
||||
|
||||
@@ -2,12 +2,15 @@ import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {dom, Observable} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
// Rather than require the whole of highlight.js, require just the core with the one language we
|
||||
// need, to keep our bundle smaller and the build faster.
|
||||
const hljs = require('highlight.js/lib/core');
|
||||
hljs.registerLanguage('python', require('highlight.js/lib/languages/python'));
|
||||
|
||||
const t = makeT('components.CodeEditorPanel');
|
||||
|
||||
export class CodeEditorPanel extends DisposableWithEvents {
|
||||
private _schema = Observable.create(this, '');
|
||||
private _denied = Observable.create(this, false);
|
||||
@@ -25,8 +28,8 @@ export class CodeEditorPanel extends DisposableWithEvents {
|
||||
return dom('div.g-code-panel.clipboard',
|
||||
{tabIndex: "-1"},
|
||||
dom.maybe(this._denied, () => dom('div.g-code-panel-denied',
|
||||
dom('h2', dom.text('Access denied')),
|
||||
dom('div', dom.text('Code View is available only when you have full document access.')),
|
||||
dom('h2', dom.text(t('AccessDenied'))),
|
||||
dom('div', dom.text(t('CodeViewOnlyFullAccess'))),
|
||||
)),
|
||||
dom.maybe(this._schema, (schema) => {
|
||||
// The reason to scope and rebuild instead of using `kd.text(schema)` is because
|
||||
|
||||
@@ -12,9 +12,12 @@ import {loadingDots} from 'app/client/ui2018/loaders';
|
||||
import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const testId = makeTestId('test-raw-data-');
|
||||
|
||||
const t = makeT('components.DataTables');
|
||||
|
||||
export class DataTables extends Disposable {
|
||||
private _tables: Observable<TableRec[]>;
|
||||
|
||||
@@ -33,7 +36,7 @@ export class DataTables extends Disposable {
|
||||
const dataTables = use(_gristDoc.docModel.rawDataTables.getObservable());
|
||||
const summaryTables = use(_gristDoc.docModel.rawSummaryTables.getObservable());
|
||||
// Remove tables that we don't have access to. ACL will remove tableId from those tables.
|
||||
return [...dataTables, ...summaryTables].filter(t => Boolean(use(t.tableId)));
|
||||
return [...dataTables, ...summaryTables].filter(table => Boolean(use(table.tableId)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +45,7 @@ export class DataTables extends Disposable {
|
||||
cssTableList(
|
||||
/*************** List section **********/
|
||||
testId('list'),
|
||||
docListHeader('Raw Data Tables'),
|
||||
docListHeader(t('RawDataTables')),
|
||||
cssList(
|
||||
dom.forEach(this._tables, tableRec =>
|
||||
cssItem(
|
||||
@@ -62,11 +65,11 @@ export class DataTables extends Disposable {
|
||||
testId('table-id'),
|
||||
dom.text(tableRec.tableId),
|
||||
),
|
||||
{ title : 'Click to copy' },
|
||||
dom.on('click', async (e, t) => {
|
||||
{ title : t('ClickToCopy') },
|
||||
dom.on('click', async (e, d) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
showTransientTooltip(t, 'Table ID copied to clipboard', {
|
||||
showTransientTooltip(d, t('TableIDCopied'), {
|
||||
key: 'copy-table-id'
|
||||
});
|
||||
await copyToClipboard(tableRec.tableId.peek());
|
||||
@@ -124,7 +127,7 @@ export class DataTables extends Disposable {
|
||||
return [
|
||||
menuItem(
|
||||
() => this._duplicateTable(table),
|
||||
'Duplicate Table',
|
||||
t('DuplicateTable'),
|
||||
testId('menu-duplicate-table'),
|
||||
dom.cls('disabled', use =>
|
||||
use(isReadonly) ||
|
||||
@@ -141,23 +144,23 @@ export class DataTables extends Disposable {
|
||||
use(docModel.visibleTables.getObservable()).length <= 1 && !use(table.isHidden)
|
||||
))
|
||||
),
|
||||
dom.maybe(isReadonly, () => menuText('You do not have edit access to this document')),
|
||||
dom.maybe(isReadonly, () => menuText(t("NoEditAccess"))),
|
||||
];
|
||||
}
|
||||
|
||||
private _duplicateTable(t: TableRec) {
|
||||
duplicateTable(this._gristDoc, t.tableId(), {
|
||||
private _duplicateTable(r: TableRec) {
|
||||
duplicateTable(this._gristDoc, r.tableId(), {
|
||||
onSuccess: ({raw_section_id}: DuplicateTableResponse) =>
|
||||
this._gristDoc.viewModel.activeSectionId(raw_section_id),
|
||||
});
|
||||
}
|
||||
|
||||
private _removeTable(t: TableRec) {
|
||||
private _removeTable(r: TableRec) {
|
||||
const {docModel} = this._gristDoc;
|
||||
function doRemove() {
|
||||
return docModel.docData.sendAction(['RemoveTable', t.tableId()]);
|
||||
return docModel.docData.sendAction(['RemoveTable', r.tableId()]);
|
||||
}
|
||||
confirmModal(`Delete ${t.formattedTableName()} data, and remove it from all pages?`, 'Delete', doRemove);
|
||||
confirmModal(t("DeleteData", {formattedTableName : r.formattedTableName()}), 'Delete', doRemove);
|
||||
}
|
||||
|
||||
private _tableRows(table: TableRec) {
|
||||
|
||||
@@ -11,6 +11,9 @@ import {Features, isFreePlan} from 'app/common/Features';
|
||||
import {capitalizeFirstWord} from 'app/common/gutil';
|
||||
import {canUpgradeOrg} from 'app/common/roles';
|
||||
import {Computed, Disposable, dom, DomContents, DomElementArg, makeTestId, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.DocumentUsage');
|
||||
|
||||
const testId = makeTestId('test-doc-usage-');
|
||||
|
||||
@@ -23,9 +26,6 @@ const DEFAULT_MAX_DATA_SIZE = DEFAULT_MAX_ROWS * 2 * 1024; // 40MB (2KiB per row
|
||||
// Default used by the progress bar to visually indicate attachments size usage.
|
||||
const DEFAULT_MAX_ATTACHMENTS_SIZE = 1 * 1024 * 1024 * 1024; // 1GiB
|
||||
|
||||
const ACCESS_DENIED_MESSAGE = 'Usage statistics are only available to users with '
|
||||
+ 'full access to the document data.';
|
||||
|
||||
/**
|
||||
* Displays statistics about document usage, such as number of rows used.
|
||||
*/
|
||||
@@ -60,7 +60,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid row limits are currently treated as if they are undefined.
|
||||
const maxValue = maxRows && maxRows > 0 ? maxRows : undefined;
|
||||
return {
|
||||
name: 'Rows',
|
||||
name: t('Rows'),
|
||||
currentValue: typeof rowCount !== 'object' ? undefined : rowCount.total,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_ROWS,
|
||||
unit: 'rows',
|
||||
@@ -75,7 +75,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid data size limits are currently treated as if they are undefined.
|
||||
const maxValue = maxSize && maxSize > 0 ? maxSize : undefined;
|
||||
return {
|
||||
name: 'Data Size',
|
||||
name: t('DataSize'),
|
||||
currentValue: typeof dataSize !== 'number' ? undefined : dataSize,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_DATA_SIZE,
|
||||
unit: 'MB',
|
||||
@@ -97,7 +97,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid attachments size limits are currently treated as if they are undefined.
|
||||
const maxValue = maxSize && maxSize > 0 ? maxSize : undefined;
|
||||
return {
|
||||
name: 'Attachments Size',
|
||||
name: t('AttachmentsSize'),
|
||||
currentValue: typeof attachmentsSize !== 'number' ? undefined : attachmentsSize,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_ATTACHMENTS_SIZE,
|
||||
unit: 'GB',
|
||||
@@ -135,7 +135,7 @@ export class DocumentUsage extends Disposable {
|
||||
|
||||
public buildDom() {
|
||||
return dom('div',
|
||||
cssHeader('Usage', testId('heading')),
|
||||
cssHeader(t('Usage'), testId('heading')),
|
||||
dom.domComputed(this._areAllMetricsPending, (isLoading) => {
|
||||
if (isLoading) { return cssSpinner(loadingSpinner(), testId('loading')); }
|
||||
|
||||
@@ -149,7 +149,7 @@ export class DocumentUsage extends Disposable {
|
||||
return dom.domComputed((use) => {
|
||||
const isAccessDenied = use(this._isAccessDenied);
|
||||
if (isAccessDenied === null) { return null; }
|
||||
if (isAccessDenied) { return buildMessage(ACCESS_DENIED_MESSAGE); }
|
||||
if (isAccessDenied) { return buildMessage(t('UsageStatisticsOnlyFullAccess')); }
|
||||
|
||||
const org = use(this._currentOrg);
|
||||
const product = use(this._currentProduct);
|
||||
@@ -237,11 +237,12 @@ export function buildUpgradeMessage(
|
||||
variant: 'short' | 'long',
|
||||
onUpgrade: () => void,
|
||||
) {
|
||||
if (!canUpgrade) { return 'Contact the site owner to upgrade the plan to raise limits.'; }
|
||||
if (!canUpgrade) { return t('LimitContactSiteOwner'); }
|
||||
|
||||
const upgradeLinkText = 'start your 30-day free trial of the Pro plan.';
|
||||
const upgradeLinkText = t('UpgradeLinkText')
|
||||
// TODO i18next
|
||||
return [
|
||||
variant === 'short' ? null : 'For higher limits, ',
|
||||
variant === 'short' ? null : t('ForHigherLimits'),
|
||||
buildUpgradeLink(
|
||||
variant === 'short' ? capitalizeFirstWord(upgradeLinkText) : upgradeLinkText,
|
||||
() => onUpgrade(),
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
IDomArgs, MultiHolder, styled, TagElem
|
||||
} from "grainjs";
|
||||
import { GristDoc } from "app/client/components/GristDoc";
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { ITooltipControl, showTooltip, tooltipCloseButton } from "app/client/ui/tooltips";
|
||||
import { FieldEditorStateEvent } from "app/client/widgets/FieldEditor";
|
||||
import { testId, theme } from "app/client/ui2018/cssVars";
|
||||
import { cssLink } from "app/client/ui2018/links";
|
||||
|
||||
const t = makeT('components.Drafts');
|
||||
|
||||
/**
|
||||
* Component that keeps track of editor's state (draft value). If user hits an escape button
|
||||
* by accident, this component will provide a way to continue the work.
|
||||
@@ -270,7 +273,7 @@ class NotificationAdapter extends Disposable implements Notification {
|
||||
}
|
||||
public showUndoDiscard() {
|
||||
const notifier = this._doc.app.topAppModel.notifier;
|
||||
const notification = notifier.createUserMessage("Undo discard", {
|
||||
const notification = notifier.createUserMessage(t("UndoDiscard"), {
|
||||
message: () =>
|
||||
discardNotification(
|
||||
dom.on("click", () => {
|
||||
@@ -418,7 +421,7 @@ const styledTooltip = styled('div', `
|
||||
function cellTooltip(clb: () => any) {
|
||||
return function (ctl: ITooltipControl) {
|
||||
return styledTooltip(
|
||||
cssLink('Restore last edit',
|
||||
cssLink(t('RestoreLastEdit'),
|
||||
dom.on('mousedown', (ev) => { ev.preventDefault(); ctl.close(); clb(); }),
|
||||
testId('draft-tooltip'),
|
||||
),
|
||||
@@ -437,7 +440,7 @@ const styledNotification = styled('div', `
|
||||
`);
|
||||
function discardNotification(...args: IDomArgs<TagElem<"div">>) {
|
||||
return styledNotification(
|
||||
"Undo Discard",
|
||||
t("UndoDiscard"),
|
||||
testId("draft-notification"),
|
||||
...args
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {ViewLayout} from 'app/client/components/ViewLayout';
|
||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
import {DocPluginManager} from 'app/client/lib/DocPluginManager';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {setTestState} from 'app/client/lib/testState';
|
||||
import {selectFiles} from 'app/client/lib/uploads';
|
||||
@@ -84,6 +85,8 @@ import * as ko from 'knockout';
|
||||
import cloneDeepWith = require('lodash/cloneDeepWith');
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('components.GristDoc');
|
||||
|
||||
const G = getBrowserGlobals('document', 'window');
|
||||
|
||||
// Re-export some tools to move them from main webpack bundle to the one with GristDoc.
|
||||
@@ -307,7 +310,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
const importSourceElems = ImportSourceElement.fromArray(this.docPluginManager.pluginsList);
|
||||
const importMenuItems = [
|
||||
{
|
||||
label: 'Import from file',
|
||||
label: t('ImportFromFile'),
|
||||
action: () => Importer.selectAndImport(this, importSourceElems, null, createPreview),
|
||||
},
|
||||
...importSourceElems.map(importSourceElem => ({
|
||||
@@ -592,7 +595,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
}
|
||||
const res = await docData.bundleActions(
|
||||
`Added new linked section to view ${viewName}`,
|
||||
t("AddedNewLinkedSection", {viewName}),
|
||||
() => this.addWidgetToPageImpl(val, tableId ?? null)
|
||||
);
|
||||
|
||||
@@ -669,7 +672,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
|
||||
return await this._viewLayout!.freezeUntil(docData.bundleActions(
|
||||
`Saved linked section ${section.title()} in view ${viewModel.name()}`,
|
||||
t("SavedLinkedSectionIn", {title:section.title(), name: viewModel.name()}),
|
||||
async () => {
|
||||
|
||||
// if table changes or a table is made a summary table, let's replace the view section by a
|
||||
|
||||
@@ -9,6 +9,7 @@ import {buildParseOptionsForm, ParseOptionValues} from 'app/client/components/Pa
|
||||
import {PluginScreen} from 'app/client/components/PluginScreen';
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {fetchURL, isDriveUrl, selectFiles, uploadFiles} from 'app/client/lib/uploads';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@@ -35,6 +36,8 @@ import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {ACCESS_DENIED, AUTH_INTERRUPTED, canReadPrivateFiles, getGoogleCodeForReading} from 'app/client/ui/googleAuth';
|
||||
import debounce = require('lodash/debounce');
|
||||
|
||||
const t = makeT('components.Importer');
|
||||
|
||||
|
||||
// We expect a function for creating the preview GridView, to avoid the need to require the
|
||||
// GridView module here. That brings many dependencies, making a simple test fixture difficult.
|
||||
@@ -628,7 +631,7 @@ export class Importer extends DisposableWithEvents {
|
||||
cssMergeOptions(
|
||||
cssMergeOptionsToggle(labeledSquareCheckbox(
|
||||
updateExistingRecords,
|
||||
'Update existing records',
|
||||
t('UpdateExistingRecords'),
|
||||
dom.autoDispose(updateRecordsListener),
|
||||
testId('importer-update-existing-records')
|
||||
)),
|
||||
@@ -643,14 +646,14 @@ export class Importer extends DisposableWithEvents {
|
||||
|
||||
return [
|
||||
cssMergeOptionsMessage(
|
||||
'Merge rows that match these fields:',
|
||||
t('MergeRowsThatMatch'),
|
||||
testId('importer-merge-fields-message')
|
||||
),
|
||||
multiSelect(
|
||||
mergeCols,
|
||||
section.viewFields().peek().map(f => ({label: f.label(), value: f.colId()})) ?? [],
|
||||
{
|
||||
placeholder: 'Select fields to match on',
|
||||
placeholder: t("SelectFieldsToMatch"),
|
||||
error: hasInvalidMergeCols
|
||||
},
|
||||
dom.autoDispose(mergeColsListener),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { bigBasicButton } from 'app/client/ui2018/buttons';
|
||||
import { testId } from 'app/client/ui2018/cssVars';
|
||||
import { loadingSpinner } from 'app/client/ui2018/loaders';
|
||||
@@ -6,6 +7,8 @@ import { PluginInstance } from 'app/common/PluginInstance';
|
||||
import { RenderTarget } from 'app/plugin/RenderOptions';
|
||||
import { Disposable, dom, DomContents, Observable, styled } from 'grainjs';
|
||||
|
||||
const t = makeT('components.PluginScreen');
|
||||
|
||||
/**
|
||||
* Rendering options for the PluginScreen modal.
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ export class PluginScreen extends Disposable {
|
||||
public renderError(message: string) {
|
||||
this.render([
|
||||
this._buildModalTitle(),
|
||||
cssModalBody('Import failed: ', message, testId('importer-error')),
|
||||
cssModalBody(t('ImportFailed'), message, testId('importer-error')),
|
||||
cssModalButtons(
|
||||
bigBasicButton('Close',
|
||||
dom.on('click', () => this.close()),
|
||||
|
||||
@@ -32,6 +32,7 @@ var dispose = require('../lib/dispose');
|
||||
var dom = require('../lib/dom');
|
||||
var {Delay} = require('../lib/Delay');
|
||||
var kd = require('../lib/koDom');
|
||||
var {makeT} = require('../lib/localization');
|
||||
var Layout = require('./Layout');
|
||||
var RecordLayoutEditor = require('./RecordLayoutEditor');
|
||||
var commands = require('./commands');
|
||||
@@ -40,6 +41,8 @@ var {menu} = require('../ui2018/menus');
|
||||
var {testId} = require('app/client/ui2018/cssVars');
|
||||
var {contextMenu} = require('app/client/ui/contextMenu');
|
||||
|
||||
const t = makeT('components.RecordLayout');
|
||||
|
||||
/**
|
||||
* Construct a RecordLayout.
|
||||
* @param {MetaRowModel} options.viewSection: The model for the viewSection represented.
|
||||
@@ -260,7 +263,7 @@ RecordLayout.prototype.saveLayoutSpec = async function(layoutSpec) {
|
||||
// Use separate copies of addColAction, since sendTableActions modified each in-place.
|
||||
let addActions = gutil.arrayRepeat(addColNum, 0).map(() => addColAction.slice());
|
||||
|
||||
await docData.bundleActions('Updating record layout.', () => {
|
||||
await docData.bundleActions(t('UpdatingRecordLayout'), () => {
|
||||
return Promise.try(() => {
|
||||
return addColNum > 0 ? docModel.dataTables[tableId].sendTableActions(addActions) : [];
|
||||
})
|
||||
|
||||
@@ -2,8 +2,11 @@ var _ = require('underscore');
|
||||
var BackboneEvents = require('backbone').Events;
|
||||
|
||||
var dispose = require('app/client/lib/dispose');
|
||||
var {makeT} = require('app/client/lib/localization');
|
||||
var commands = require('./commands');
|
||||
var LayoutEditor = require('./LayoutEditor');
|
||||
|
||||
const t = makeT('components.RecordLayoutEditor');
|
||||
const {basicButton, cssButton, primaryButton} = require('app/client/ui2018/buttons');
|
||||
const {icon} = require('app/client/ui2018/icons');
|
||||
const {menu, menuDivider, menuItem} = require('app/client/ui2018/menus');
|
||||
@@ -90,13 +93,13 @@ RecordLayoutEditor.prototype.buildEditorDom = function() {
|
||||
};
|
||||
|
||||
return cssControls(
|
||||
basicButton('Add Field', cssCollapseIcon('Collapse'),
|
||||
basicButton(t('AddField'), cssCollapseIcon('Collapse'),
|
||||
menu((ctl) => [
|
||||
menuItem(() => addNewField(), 'Create New Field'),
|
||||
menuItem(() => addNewField(), t('CreateNewField')),
|
||||
dom.maybe((use) => use(this._hiddenColumns).length > 0,
|
||||
() => menuDivider()),
|
||||
dom.forEach(this._hiddenColumns, (col) =>
|
||||
menuItem(() => showField(col), `Show field ${col.label()}`)
|
||||
menuItem(() => showField(col), t("ShowField", {label:col.label()}))
|
||||
),
|
||||
testId('edit-layout-add-menu'),
|
||||
]),
|
||||
@@ -110,10 +113,10 @@ RecordLayoutEditor.prototype.buildEditorDom = function() {
|
||||
|
||||
RecordLayoutEditor.prototype.buildFinishButtons = function() {
|
||||
return [
|
||||
primaryButton('Save Layout',
|
||||
primaryButton(t('SaveLayout'),
|
||||
dom.on('click', () => commands.allCommands.accept.run()),
|
||||
),
|
||||
basicButton('Cancel',
|
||||
basicButton(t('Cancel'),
|
||||
dom.on('click', () => commands.allCommands.cancel.run()),
|
||||
{style: 'margin-left: 8px'},
|
||||
),
|
||||
|
||||
@@ -12,6 +12,9 @@ import * as gutil from 'app/common/gutil';
|
||||
import {Disposable, dom, fromKo, styled} from 'grainjs';
|
||||
import ko from 'knockout';
|
||||
import {menu, menuItem} from 'popweasel';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.RefSelect');
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
@@ -44,8 +47,8 @@ export class RefSelect extends Disposable {
|
||||
// Indicates whether this is a ref col that references a different table.
|
||||
// (That's the only time when RefSelect is offered.)
|
||||
this.isForeignRefCol = this.autoDispose(ko.computed(() => {
|
||||
const t = this._origColumn.refTable();
|
||||
return Boolean(t && t.getRowId() !== this._origColumn.parentId());
|
||||
const table = this._origColumn.refTable();
|
||||
return Boolean(table && table.getRowId() !== this._origColumn.parentId());
|
||||
}));
|
||||
|
||||
// Computed for the current fieldBuilder's field, if it exists.
|
||||
@@ -94,7 +97,7 @@ export class RefSelect extends Disposable {
|
||||
testId('ref-select-item'),
|
||||
)
|
||||
),
|
||||
cssAddLink(cssAddIcon('Plus'), 'Add Column',
|
||||
cssAddLink(cssAddIcon('Plus'), t('AddColumn'),
|
||||
menu(() => [
|
||||
...this._validCols.peek()
|
||||
.filter((col) => !this._addedSet.peek().has(col.colId.peek()))
|
||||
@@ -102,7 +105,7 @@ export class RefSelect extends Disposable {
|
||||
menuItem(() => this._addFormulaField({ label: col.label(), value: col.colId() }),
|
||||
col.label.peek())
|
||||
),
|
||||
cssEmptyMenuText("No columns to add"),
|
||||
cssEmptyMenuText(t("NoColumnsAdd")),
|
||||
testId('ref-select-menu'),
|
||||
]),
|
||||
testId('ref-select-add'),
|
||||
|
||||
@@ -14,6 +14,9 @@ import {TableData} from "app/common/TableData";
|
||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||
import ko from 'knockout';
|
||||
import {Computed, Disposable, dom, makeTestId, Observable, styled, subscribe} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.SelectionSummary');
|
||||
|
||||
/**
|
||||
* A beginning and end index for a range of columns or rows.
|
||||
@@ -263,7 +266,7 @@ export class SelectionSummary extends Disposable {
|
||||
|
||||
async function doCopy(value: string, elem: Element) {
|
||||
await copyToClipboard(value);
|
||||
showTransientTooltip(elem, 'Copied to clipboard', {key: 'copy-selection-summary'});
|
||||
showTransientTooltip(elem, t('CopiedClipboard'), {key: 'copy-selection-summary'});
|
||||
}
|
||||
|
||||
const cssSummary = styled('div', `
|
||||
|
||||
@@ -17,6 +17,9 @@ import {FieldBuilder} from 'app/client/widgets/FieldBuilder';
|
||||
import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
||||
import {UserAction} from 'app/common/DocActions';
|
||||
import {Computed, dom, fromKo, Observable} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.TypeTransformation');
|
||||
|
||||
// To simplify diff (avoid rearranging methods to satisfy private/public order).
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
@@ -61,25 +64,25 @@ export class TypeTransform extends ColumnTransform {
|
||||
),
|
||||
cssButtonRow(
|
||||
basicButton(dom.on('click', () => { this.cancel().catch(reportError); disableButtons.set(true); }),
|
||||
'Cancel', testId("type-transform-cancel"),
|
||||
t('Cancel'), testId("type-transform-cancel"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
),
|
||||
dom.domComputed(this._reviseTypeChange, revising => {
|
||||
if (revising) {
|
||||
return basicButton(dom.on('click', () => this.editor.writeObservable()),
|
||||
'Preview', testId("type-transform-update"),
|
||||
t('Preview'), testId("type-transform-update"),
|
||||
dom.cls('disabled', (use) => use(disableButtons) || use(this.formulaUpToDate)),
|
||||
{ title: 'Update formula (Shift+Enter)' }
|
||||
{ title: t('UpdateFormula') }
|
||||
);
|
||||
} else {
|
||||
return basicButton(dom.on('click', () => { this._reviseTypeChange.set(true); }),
|
||||
'Revise', testId("type-transform-revise"),
|
||||
t('Revise'), testId("type-transform-revise"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
);
|
||||
}
|
||||
}),
|
||||
primaryButton(dom.on('click', () => { this.execute().catch(reportError); disableButtons.set(true); }),
|
||||
'Apply', testId("type-transform-apply"),
|
||||
t('Apply'), testId("type-transform-apply"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ var dom = require('../lib/dom');
|
||||
var kd = require('../lib/koDom');
|
||||
var kf = require('../lib/koForm');
|
||||
var AceEditor = require('./AceEditor');
|
||||
var {makeT} = require('app/client/lib/localization');
|
||||
|
||||
const t = makeT('components.ValidationPanel');
|
||||
|
||||
/**
|
||||
* Document level configuration settings.
|
||||
@@ -30,7 +33,7 @@ dispose.makeDisposable(ValidationPanel);
|
||||
ValidationPanel.prototype.onAddRule = function() {
|
||||
this.validationsTable.sendTableAction(["AddRecord", null, {
|
||||
tableRef: this.docTables.at(0).id(),
|
||||
name: "Rule " + (this.validations.peekLength + 1),
|
||||
name: t("RuleLength", {length: this.validations.peekLength + 1}),
|
||||
formula: ""
|
||||
}])
|
||||
.then(function() {
|
||||
@@ -83,7 +86,7 @@ ValidationPanel.prototype.buildDom = function() {
|
||||
2, '',
|
||||
1, kf.buttonGroup(
|
||||
kf.button(() => editor.writeObservable(),
|
||||
'Apply', { title: 'Update formula (Shift+Enter)' },
|
||||
'Apply', { title: t('UpdateFormula')},
|
||||
kd.toggleClass('disabled', editorUpToDate)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -24,9 +24,12 @@ const {confirmModal} = require('app/client/ui2018/modals');
|
||||
const {Sort} = require('app/common/SortSpec');
|
||||
const isEqual = require('lodash/isEqual');
|
||||
const {cssMenuItem} = require('popweasel');
|
||||
const {makeT} = require('app/client/lib/localization');
|
||||
|
||||
const testId = makeTestId('test-vconfigtab-');
|
||||
|
||||
const t = makeT('components.ViewConfigTab');
|
||||
|
||||
/**
|
||||
* Helper class that combines one ViewSection's data for building dom.
|
||||
*/
|
||||
@@ -132,21 +135,21 @@ ViewConfigTab.prototype.buildSortDom = function() {
|
||||
cssRow(
|
||||
cssExtraMarginTop.cls(''),
|
||||
grainjsDom.maybe(hasChanged, () => [
|
||||
primaryButton('Save', {style: 'margin-right: 8px;'},
|
||||
primaryButton(t('Save'), {style: 'margin-right: 8px;'},
|
||||
grainjsDom.on('click', () => { section.activeSortJson.save(); }),
|
||||
testId('sort-save'),
|
||||
grainjsDom.boolAttr('disabled', this.gristDoc.isReadonly),
|
||||
),
|
||||
// Let's use same label (revert) as the similar button which appear in the view section.
|
||||
// menu.
|
||||
basicButton('Revert',
|
||||
basicButton(t('Revert'),
|
||||
grainjsDom.on('click', () => { section.activeSortJson.revert(); }),
|
||||
testId('sort-reset')
|
||||
)
|
||||
]),
|
||||
cssFlex(),
|
||||
grainjsDom.maybe(section.isSorted, () =>
|
||||
basicButton('Update Data', {style: 'margin-left: 8px; white-space: nowrap;'},
|
||||
basicButton(t('UpdateData'), {style: 'margin-left: 8px; white-space: nowrap;'},
|
||||
grainjsDom.on('click', () => { updatePositions(this.gristDoc, section); }),
|
||||
testId('sort-update'),
|
||||
grainjsDom.show((use) => use(use(section.table).supportsManualSort)),
|
||||
@@ -200,9 +203,9 @@ ViewConfigTab.prototype._buildSortRow = function(colRef, sortSpec, columns) {
|
||||
});
|
||||
return {computed, allowedTypes, flag, label};
|
||||
}
|
||||
const orderByChoice = computedFlag('orderByChoice', ['Choice'], 'Use choice position');
|
||||
const naturalSort = computedFlag('naturalSort', ['Text'], 'Natural sort');
|
||||
const emptyLast = computedFlag('emptyLast', null, 'Empty values last');
|
||||
const orderByChoice = computedFlag('orderByChoice', ['Choice'], t('UseChoicePosition'));
|
||||
const naturalSort = computedFlag('naturalSort', ['Text'], t('NaturalSort'));
|
||||
const emptyLast = computedFlag('emptyLast', null, t('EmptyValuesLast'));
|
||||
const flags = [orderByChoice, emptyLast, naturalSort];
|
||||
|
||||
const column = columns.get().find(col => col.value === Sort.getColRef(colRef));
|
||||
@@ -278,7 +281,7 @@ ViewConfigTab.prototype._buildAddToSortBtn = function(columns) {
|
||||
grainjsDom.autoDispose(showAddNew),
|
||||
grainjsDom.autoDispose(available),
|
||||
cssTextBtn(
|
||||
cssPlusIcon('Plus'), 'Add Column',
|
||||
cssPlusIcon('Plus'), t('AddColumn'),
|
||||
testId('sort-add')
|
||||
),
|
||||
grainjsDom.hide((use) => use(showAddNew) || !use(available).length),
|
||||
@@ -304,7 +307,7 @@ ViewConfigTab.prototype._buildAddToSortBtn = function(columns) {
|
||||
return cssRow(cssSortRow(
|
||||
dom.autoDispose(col),
|
||||
cssSortSelect(
|
||||
select(col, [], {defaultLabel: 'Add Column'}),
|
||||
select(col, [], {defaultLabel: t('AddColumn')}),
|
||||
menu(() => [
|
||||
menuCols,
|
||||
grainjsDom.onDispose(() => { showAddNew.set(false); })
|
||||
@@ -372,9 +375,9 @@ ViewConfigTab.prototype._buildAdvancedSettingsDom = function() {
|
||||
const table = sectionData.section.table();
|
||||
const isCollapsed = ko.observable(true);
|
||||
return [
|
||||
kf.collapserLabel(isCollapsed, 'Advanced settings', dom.testId('ViewConfig_advanced')),
|
||||
kf.collapserLabel(isCollapsed, t('AdvancedSettings'), dom.testId('ViewConfig_advanced')),
|
||||
kf.helpRow(kd.hide(isCollapsed),
|
||||
'Big tables may be marked as "on-demand" to avoid loading them into the data engine.',
|
||||
t('BigTablesMayBeMarked'),
|
||||
kd.style('text-align', 'left'),
|
||||
kd.style('margin-top', '1.5rem')
|
||||
),
|
||||
@@ -383,7 +386,7 @@ ViewConfigTab.prototype._buildAdvancedSettingsDom = function() {
|
||||
),
|
||||
kf.row(kd.hide(isCollapsed),
|
||||
kf.buttonGroup(kf.button(() => this._makeOnDemand(table),
|
||||
kd.text(() => table.onDemand() ? 'Unmark On-Demand' : 'Make On-Demand'),
|
||||
kd.text(() => table.onDemand() ? t('UnmarkOnDemandButton') : t('MakeOnDemandButton')),
|
||||
dom.testId('ViewConfig_onDemandBtn')
|
||||
))
|
||||
),
|
||||
@@ -401,7 +404,7 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
const hasChangedObs = Computed.create(null, (use) => use(section.filterSpecChanged) || !use(section.activeFilterBar.isSaved))
|
||||
|
||||
async function save() {
|
||||
await docModel.docData.bundleActions("Update Filter settings", () => Promise.all([
|
||||
await docModel.docData.bundleActions(t("UpdateFilterSettings"), () => Promise.all([
|
||||
section.saveFilters(), // Save filter
|
||||
section.activeFilterBar.save(), // Save bar
|
||||
]));
|
||||
@@ -438,7 +441,7 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
grainjsDom.domComputed((use) => {
|
||||
const filters = use(section.filters);
|
||||
return cssTextBtn(
|
||||
cssPlusIcon('Plus'), 'Add Filter',
|
||||
cssPlusIcon('Plus'), t('AddFilter'),
|
||||
addFilterMenu(filters, section, popupControls, {placement: 'bottom-end'}),
|
||||
testId('add-filter-btn'),
|
||||
);
|
||||
@@ -462,12 +465,12 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
cssExtraMarginTop.cls(''),
|
||||
testId('save-filter-btns'),
|
||||
primaryButton(
|
||||
'Save', {style: 'margin-right: 8px'},
|
||||
t('Save'), {style: 'margin-right: 8px'},
|
||||
grainjsDom.on('click', save),
|
||||
grainjsDom.boolAttr('disabled', this.gristDoc.isReadonly),
|
||||
),
|
||||
basicButton(
|
||||
'Revert',
|
||||
t('Revert'),
|
||||
grainjsDom.on('click', revert),
|
||||
)
|
||||
))
|
||||
@@ -484,9 +487,9 @@ ViewConfigTab.prototype._buildThemeDom = function() {
|
||||
return cssRow(
|
||||
dom.autoDispose(theme),
|
||||
select(theme, [
|
||||
{label: 'Form', value: 'form' },
|
||||
{label: 'Compact', value: 'compact'},
|
||||
{label: 'Blocks', value: 'blocks' },
|
||||
{label: t('Form'), value: 'form' },
|
||||
{label: t('Compact'), value: 'compact'},
|
||||
{label: t('Blocks'), value: 'blocks' },
|
||||
]),
|
||||
testId('detail-theme')
|
||||
);
|
||||
@@ -505,7 +508,7 @@ ViewConfigTab.prototype._buildLayoutDom = function() {
|
||||
const layoutEditorObs = ko.computed(() => view && view.recordLayout && view.recordLayout.layoutEditor());
|
||||
return cssRow({style: 'margin-top: 16px;'},
|
||||
kd.maybe(layoutEditorObs, (editor) => editor.buildFinishButtons()),
|
||||
primaryButton('Edit Card Layout',
|
||||
primaryButton(t('EditCardLayout'),
|
||||
dom.autoDispose(layoutEditorObs),
|
||||
dom.on('click', () => commands.allCommands.editLayout.run()),
|
||||
grainjsDom.hide(layoutEditorObs),
|
||||
@@ -553,8 +556,8 @@ ViewConfigTab.prototype._buildCustomTypeItems = function() {
|
||||
// 3)
|
||||
showObs: () => activeSection().customDef.mode() === "plugin",
|
||||
buildDom: () => kd.scope(activeSection, ({customDef}) => dom('div',
|
||||
kf.row(5, "Plugin: ", 13, kf.text(customDef.pluginId, {}, {list: "list_plugin"}, dom.testId('ViewConfigTab_customView_pluginId'))),
|
||||
kf.row(5, "Section: ", 13, kf.text(customDef.sectionId, {}, {list: "list_section"}, dom.testId('ViewConfigTab_customView_sectionId'))),
|
||||
kf.row(5, t("PluginColon"), 13, kf.text(customDef.pluginId, {}, {list: "list_plugin"}, dom.testId('ViewConfigTab_customView_pluginId'))),
|
||||
kf.row(5, t("SectionColon"), 13, kf.text(customDef.sectionId, {}, {list: "list_section"}, dom.testId('ViewConfigTab_customView_sectionId'))),
|
||||
// For both `customPlugin` and `selectedSection` it is possible for the value not to be in the
|
||||
// list of options. Combining <datalist> and <input> allows both to freely edit the value with
|
||||
// keyboard and to select it from a list. Although the content of the list seems to be
|
||||
|
||||
@@ -13,6 +13,9 @@ import flatten = require('lodash/flatten');
|
||||
import forEach = require('lodash/forEach');
|
||||
import zip = require('lodash/zip');
|
||||
import zipObject = require('lodash/zipObject');
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.duplicatePage');
|
||||
|
||||
// Duplicate page with pageId. Starts by prompting user for a new name.
|
||||
export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
||||
@@ -27,8 +30,7 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
||||
cssLabel("Name"),
|
||||
inputEl = cssInput({value: pageName + ' (copy)'}),
|
||||
),
|
||||
"Note that this does not copy data, ",
|
||||
"but creates another view of the same data.",
|
||||
t("DoesNotCopyData"),
|
||||
])
|
||||
));
|
||||
}
|
||||
@@ -39,7 +41,7 @@ async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: strin
|
||||
const viewSections = sourceView.viewSections.peek().peek();
|
||||
let viewRef = 0;
|
||||
await gristDoc.docData.bundleActions(
|
||||
`Duplicate page ${pageName}`,
|
||||
t("DuplicatePageName", {pageName}),
|
||||
async () => {
|
||||
// create new view and new sections
|
||||
const results = await createNewViewSections(gristDoc.docData, viewSections);
|
||||
|
||||
Reference in New Issue
Block a user