diff --git a/app/client/ui/welcomeTour.ts b/app/client/ui/welcomeTour.ts index 2b6610f6..2cde51e7 100644 --- a/app/client/ui/welcomeTour.ts +++ b/app/client/ui/welcomeTour.ts @@ -1,3 +1,4 @@ +import { makeT } from 'app/client/lib/localization'; import * as commands from 'app/client/components/commands'; import { urlState } from 'app/client/models/gristUrlState'; import { IOnBoardingMsg, startOnBoarding } from "app/client/ui/OnBoardingPopups"; @@ -6,25 +7,26 @@ import { icon } from "app/client/ui2018/icons"; import { cssLink } from "app/client/ui2018/links"; import { dom, styled } from "grainjs"; +const t = makeT('WelcomeTour'); + export const welcomeTour: IOnBoardingMsg[] = [ { - title: 'Editing Data', + title: t('Editing Data'), body: () => [ dom('p', - 'Double-click or hit ', Key(KeyContent('Enter')), ' on a cell to edit it. ', - 'Start with ', Key(KeyStrong('=')), ' to enter a formula.' - ) + t('Double-click or hit {{enter}} on a cell to edit it. ', {enter: Key(KeyContent(t('Enter')))}), + t('Start with {{equal}} to enter a formula.', { equal: Key(KeyStrong('=')) })) ], selector: '.field_clip', placement: 'bottom', }, { selector: '.tour-creator-panel', - title: 'Configuring your document', + title: t('Configuring your document'), body: () => [ dom('p', - 'Toggle the ', dom('em', 'creator panel'), ' to format columns, ', - 'convert to card view, select data, and more.' + t('Toggle the {{creatorPanel}} to format columns, ', {creatorPanel: dom('em', t('creator panel'))}), + t('convert to card view, select data, and more.') ) ], placement: 'left', @@ -32,50 +34,52 @@ export const welcomeTour: IOnBoardingMsg[] = [ }, { selector: '.tour-type-selector', - title: 'Customizing columns', + title: t('Customizing columns'), body: () => [ dom('p', - 'Set formatting options, formulas, or column types, such as dates, choices, or attachments. '), + t('Set formatting options, formulas, or column types, such as dates, choices, or attachments. ')), dom('p', - 'Make it relational! Use the ', Key('Reference'), ' type to link tables. ' + t('Make it relational! Use the {{ref}} type to link tables. ', {ref: Key(t('Reference'))}), ) ], placement: 'right', }, { selector: '.tour-add-new', - title: 'Building up', + title: t('Building up'), body: () => [ - dom('p', 'Use ', Key('Add New'), ' to add widgets, pages, or import more data. ') + dom('p', t('Use {{addNew}} to add widgets, pages, or import more data. ', {addNew: Key(t('Add New'))})) ], placement: 'right', }, { selector: '.tour-share-icon', - title: 'Sharing', + title: t('Sharing'), body: () => [ - dom('p', 'Use the Share button (', TopBarButtonIcon('Share'), ') to share the document or export data.') + dom('p', t('Use the Share button ({{share}}) to share the document or export data.', + {share:TopBarButtonIcon(t('Share'))})) ], placement: 'bottom', cropPadding: true, }, { selector: '.tour-help-center', - title: 'Flying higher', + title: t('Flying higher'), body: () => [ - dom('p', 'Use ', Key(GreyIcon('Help'), 'Help Center'), ' for documentation or questions.'), + dom('p', t('Use {{helpCenter}} for documentation or questions.', {helpCenter: Key(GreyIcon('Help'), t('Help Center'))})), ], placement: 'right', }, { selector: '.tour-welcome', - title: 'Welcome to Grist!', + title: t('Welcome to Grist!'), body: () => [ - dom('p', 'Browse our ', - cssLink({target: '_blank', href: urlState().makeUrl({homePage: "templates"})}, - 'template library', cssInlineIcon('FieldLink')), - "to discover what's possible and get inspired." - ), + dom('p', t("Browse our {{templateLibrary}} to discover what's possible and get inspired.", + { + templateLibrary: cssLink({ target: '_blank', href: urlState().makeUrl({ homePage: "templates" }) }, + t('template library'), cssInlineIcon('FieldLink')) + } + )), ], showHasModal: true, } diff --git a/app/client/widgets/CellStyle.ts b/app/client/widgets/CellStyle.ts index 761bbb66..5b318edc 100644 --- a/app/client/widgets/CellStyle.ts +++ b/app/client/widgets/CellStyle.ts @@ -1,3 +1,4 @@ +import { makeT } from 'app/client/lib/localization'; import {allCommands} from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; @@ -7,6 +8,8 @@ import {theme, vars} from 'app/client/ui2018/cssVars'; import {ConditionalStyle} from 'app/client/widgets/ConditionalStyle'; import {Computed, Disposable, dom, DomContents, fromKo, styled} from 'grainjs'; +const t = makeT('CellStyle'); + export class CellStyle extends Disposable { constructor( @@ -20,8 +23,8 @@ export class CellStyle extends Disposable { public buildDom(): DomContents { return [ cssLine( - cssLabel('CELL STYLE'), - cssButton('Open row styles', dom.on('click', allCommands.viewTabOpen.run)), + cssLabel(t('CELL STYLE')), + cssButton(t('Open row styles'), dom.on('click', allCommands.viewTabOpen.run)), ), cssRow( dom.domComputedOwned(fromKo(this._field.config.style), (holder, options) => { @@ -58,12 +61,12 @@ export class CellStyle extends Disposable { }, { onSave: () => options.save(), onRevert: () => options.revert(), - placeholder: use => use(hasMixedStyle) ? 'Mixed style' : 'Default cell style' + placeholder: use => use(hasMixedStyle) ? t('Mixed style') : t('Default cell style') } ); }), ), - dom.create(ConditionalStyle, "Cell Style", this._field, this._gristDoc, fromKo(this._field.config.multiselect)) + dom.create(ConditionalStyle, t("Cell Style"), this._field, this._gristDoc, fromKo(this._field.config.multiselect)) ]; } } diff --git a/app/client/widgets/ChoiceTextBox.ts b/app/client/widgets/ChoiceTextBox.ts index 90f2863b..e8635d70 100644 --- a/app/client/widgets/ChoiceTextBox.ts +++ b/app/client/widgets/ChoiceTextBox.ts @@ -1,3 +1,4 @@ +import {makeT} from 'app/client/lib/localization'; import {DataRowModel} from 'app/client/models/DataRowModel'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {KoSaveableObservable} from 'app/client/models/modelUtil'; @@ -13,6 +14,8 @@ export type IChoiceOptions = Style export type ChoiceOptions = Record; export type ChoiceOptionsByName = Map; +const t = makeT('ChoiceTextBox'); + export function getRenderFillColor(choiceOptions?: IChoiceOptions) { return choiceOptions?.fillColor ?? DEFAULT_FILL_COLOR; } @@ -78,7 +81,7 @@ export class ChoiceTextBox extends NTextBox { ); return [ super.buildConfigDom(), - cssLabel('CHOICES'), + cssLabel(t('CHOICES')), cssRow( dom.autoDispose(disabled), dom.autoDispose(mixed), diff --git a/app/client/widgets/ConditionalStyle.ts b/app/client/widgets/ConditionalStyle.ts index 66666f6d..7ae0f87c 100644 --- a/app/client/widgets/ConditionalStyle.ts +++ b/app/client/widgets/ConditionalStyle.ts @@ -1,3 +1,4 @@ +import {makeT} from 'app/client/lib/localization'; import {GristDoc} from 'app/client/components/GristDoc'; import {ColumnRec} from 'app/client/models/DocModel'; import {KoSaveableObservable} from 'app/client/models/modelUtil'; @@ -18,6 +19,7 @@ import {Computed, Disposable, dom, DomContents, makeTestId, Observable, styled} import debounce = require('lodash/debounce'); const testId = makeTestId('test-widget-style-'); +const t = makeT('ConditionalStyle'); export class ConditionalStyle extends Disposable { // Holds data from currently selected record (holds data only when this field has conditional styles). @@ -71,12 +73,12 @@ export class ConditionalStyle extends Disposable { { style: 'margin-top: 16px' }, withInfoTooltip( textButton( - 'Add conditional style', + t('Add conditional style'), testId('add-conditional-style'), dom.on('click', () => this._ruleOwner.addEmptyRule()), dom.prop('disabled', this._disabled), ), - (this._label === 'Row Style' + (this._label === t('Row Style') ? GristTooltips.addRowConditionalStyle() : GristTooltips.addColumnConditionalStyle() ), @@ -113,8 +115,8 @@ export class ConditionalStyle extends Disposable { const errorMessage = Computed.create(owner, use => { const value = use(currentValue); return (!use(hasError) ? '' : - isRaisedException(value) ? 'Error in style rule' : - 'Rule must return True or False'); + isRaisedException(value) ? t('Error in style rule') : + t('Rule must return True or False')); }); return dom('div', testId(`conditional-rule-${ruleIndex}`), @@ -153,7 +155,7 @@ export class ConditionalStyle extends Disposable { ) ), cssRow( - textButton('Add another rule', + textButton(t('Add another rule'), dom.on('click', () => this._ruleOwner.addEmptyRule()), testId('add-another-rule'), dom.prop('disabled', use => this._disabled && use(this._disabled)) diff --git a/app/client/widgets/CurrencyPicker.ts b/app/client/widgets/CurrencyPicker.ts index b5d68c84..3551d082 100644 --- a/app/client/widgets/CurrencyPicker.ts +++ b/app/client/widgets/CurrencyPicker.ts @@ -1,9 +1,12 @@ +import { makeT } from 'app/client/lib/localization'; import {ACSelectItem, buildACSelect} from "app/client/lib/ACSelect"; import {Computed, IDisposableOwner, Observable} from "grainjs"; import {ACIndexImpl} from "app/client/lib/ACIndex"; import {testId} from 'app/client/ui2018/cssVars'; import {currencies} from 'app/common/Locales'; +const t = makeT('CurrencyPicker'); + interface CurrencyPickerOptions { // The label to use in the select menu for the default option. defaultCurrencyLabel: string; @@ -40,7 +43,7 @@ export function buildCurrencyPicker( save(_, item: ACSelectItem | undefined) { // Save only if we have found a match if (!item) { - throw new Error("Invalid currency"); + throw new Error(t("Invalid currency")); } // For default value, return undefined to use default currency for document. onSave(item.value === defaultCurrencyLabel ? undefined : item.value); diff --git a/app/client/widgets/DiscussionEditor.ts b/app/client/widgets/DiscussionEditor.ts index 1cde61e2..07d40b45 100644 --- a/app/client/widgets/DiscussionEditor.ts +++ b/app/client/widgets/DiscussionEditor.ts @@ -1,4 +1,5 @@ import {GristDoc} from 'app/client/components/GristDoc'; +import {makeT} from 'app/client/lib/localization'; import {FocusLayer} from 'app/client/lib/FocusLayer'; import {createObsArray} from 'app/client/lib/koArrayWrap'; import {localStorageBoolObs} from 'app/client/lib/localStorageObs'; @@ -35,6 +36,7 @@ import maxSize from 'popper-max-size-modifier'; import flatMap = require('lodash/flatMap'); const testId = makeTestId('test-discussion-'); +const t = makeT('DiscussionEditor'); const COMMENTS_LIMIT = 200; interface DiscussionPopupProps { @@ -68,7 +70,7 @@ export class CellWithComments extends Disposable implements ICellView { public async reply(comment: CellRec, text: string): Promise { const author = commentAuthor(this.gristDoc); - await this.gristDoc.docData.bundleActions("Reply to a comment", () => Promise.all([ + await this.gristDoc.docData.bundleActions(t("Reply to a comment"), () => Promise.all([ this.gristDoc.docModel.cells.sendTableAction([ "AddRecord", null, @@ -150,7 +152,7 @@ export class EmptyCell extends CellWithComments implements ICellView { }) } ]; - await props.gristDoc.docData.sendActions([addComment], 'Started discussion'); + await props.gristDoc.docData.sendActions([addComment], t('Started discussion')); } } @@ -221,9 +223,9 @@ class EmptyCellView extends Disposable { text: this._newText, onSave: () => this.props.onSave(this._newText.get()), onCancel: () => this.props.closeClicked?.(), - editorArgs: [{placeholder: 'Write a comment'}], - mainButton: 'Comment', - buttons: ['Cancel'], + editorArgs: [{placeholder: t('Write a comment')}], + mainButton: t('Comment'), + buttons: [t('Cancel')], args: [testId('editor-start')] })); } @@ -274,7 +276,7 @@ class CellWithCommentsView extends Disposable implements IDomComponent { public buildDom() { return cssTopic( - dom.maybe(this._truncated, () => cssTruncate(`Showing last ${COMMENTS_LIMIT} comments`)), + dom.maybe(this._truncated, () => cssTruncate(t("Showing last {{nb}} comments", {nb: COMMENTS_LIMIT}))), cssTopic.cls('-panel', this.props.panel), domOnCustom(CommentView.EDIT, (s: CommentView) => this._onEditComment(s)), domOnCustom(CommentView.CANCEL, (s: CommentView) => this._onCancelEdit()), @@ -358,7 +360,7 @@ class CellWithCommentsView extends Disposable implements IDomComponent { onSave: () => this._save(), onCancel: () => this.props.closeClicked?.(), mainButton: 'Send', - editorArgs: [{placeholder: 'Comment'}], + editorArgs: [{placeholder: t('Comment')}], args: [testId('editor-add')] })); } @@ -464,8 +466,8 @@ class CommentView extends Disposable { const text = Observable.create(owner, comment.text.peek() ?? ''); return dom.create(CommentEntry, { text, - mainButton: 'Save', - buttons: ['Cancel'], + mainButton: t('Save'), + buttons: [t('Cancel')], onSave: async () => { const value = text.get(); text.set(""); @@ -503,7 +505,7 @@ class CommentView extends Disposable { dom.maybe(use => !use(this.isEditing) && !this.props.isReply && !use(comment.resolved), () => dom.domComputed(use => { if (!use(this.replying)) { - return cssReplyButton(icon('Message'), 'Reply', + return cssReplyButton(icon('Message'), t('Reply'), testId('comment-reply-button'), dom.on('click', withStop(() => this.replying.set(true))), dom.style('margin-left', use2 => use2(this._hasReplies) ? '16px' : '0px'), @@ -513,8 +515,8 @@ class CommentView extends Disposable { return dom.create(CommentEntry, { text, args: [dom.style('margin-top', '8px'), testId('editor-reply')], - mainButton: 'Reply', - buttons: ['Cancel'], + mainButton: t('Reply'), + buttons: [t('Cancel')], onSave: async () => { const value = text.get(); this.replying.set(false); @@ -522,7 +524,7 @@ class CommentView extends Disposable { }, onCancel: () => this.replying.set(false), onClick: (button) => { - if (button === 'Cancel') { + if (button === t('Cancel')) { this.replying.set(false); } }, @@ -538,7 +540,7 @@ class CommentView extends Disposable { testId('comment-resolved'), icon('FieldChoice'), cssResolvedText(dom.text( - `Marked as resolved` + t(`Marked as resolved`) ))); }), ]), @@ -554,23 +556,23 @@ class CommentView extends Disposable { !canResolve ? null : menuItem( () => this.props.topic.resolve(this.props.comment), - 'Resolve' + t('Resolve') ), !comment.resolved() ? null : menuItem( () => this.props.topic.open(comment), - 'Open' + t('Open') ), menuItem( () => this.props.topic.remove(comment), - 'Remove', + t('Remove'), dom.cls('disabled', use => { return currentUser !== use(comment.userRef); }) ), menuItem( () => this._edit(), - 'Edit', + t('Edit'), dom.cls('disabled', use => { return currentUser !== use(comment.userRef); }) @@ -605,7 +607,7 @@ class CommentEntry extends Disposable { public buildDom() { const text = this.props.text; const clickBuilder = (button: string) => dom.on('click', () => { - if (button === "Cancel") { + if (button === t("Cancel")) { this.props.onCancel?.(); } else { this.props.onClick?.(button); @@ -699,9 +701,9 @@ export class DiscussionPanel extends Disposable implements IDomComponent { const tables = Computed.create(owner, use => { // Filter out those tables that are not available by ACL. if (use(this._currentPageKo)) { - return [...new Set(use(viewSections).map(vs => use(vs.table)).filter(t => use(t.tableId)))]; + return [...new Set(use(viewSections).map(vs => use(vs.table)).filter(tb => use(tb.tableId)))]; } else { - return use(this._grist.docModel.visibleTables.getObservable()).filter(t => use(t.tableId)); + return use(this._grist.docModel.visibleTables.getObservable()).filter(tb => use(tb.tableId)); } }); @@ -774,8 +776,8 @@ export class DiscussionPanel extends Disposable implements IDomComponent { ; }); const allDiscussions = Computed.create(owner, use => { - const list = flatMap(flatMap(use(tables).map(t => { - const columns = use(use(t.columns).getObservable()); + const list = flatMap(flatMap(use(tables).map(tb => { + const columns = use(use(tb.columns).getObservable()); const dList = columns.map(col => use(use(col.cells).getObservable()) .filter(c => use(c.root) && use(c.type) === CellInfoType.COMMENT)); return dList; @@ -818,9 +820,9 @@ export class DiscussionPanel extends Disposable implements IDomComponent { testId('panel-menu'), menu(() => { return [cssDropdownMenu( - labeledSquareCheckbox(this._onlyMine, "Only my threads", testId('my-threads')), - labeledSquareCheckbox(this._currentPage, "Only current page", testId('only-page')), - labeledSquareCheckbox(this._resolved, "Show resolved comments", testId('show-resolved')), + labeledSquareCheckbox(this._onlyMine, t("Only my threads"), testId('my-threads')), + labeledSquareCheckbox(this._currentPage, t("Only current page"), testId('only-page')), + labeledSquareCheckbox(this._resolved, t("Show resolved comments"), testId('show-resolved')), )]; }, {placement: 'bottom-start'}), dom.on('click', stopPropagation) diff --git a/app/client/widgets/EditorTooltip.ts b/app/client/widgets/EditorTooltip.ts index 49457b87..ddf38ea2 100644 --- a/app/client/widgets/EditorTooltip.ts +++ b/app/client/widgets/EditorTooltip.ts @@ -1,13 +1,16 @@ +import {makeT} from 'app/client/lib/localization'; import {ITooltipControl, showTooltip, tooltipCloseButton} from 'app/client/ui/tooltips'; import {colors, testId} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {cssLink} from 'app/client/ui2018/links'; import {dom, styled} from 'grainjs'; +const t = makeT('DiscussionEditor'); + export function showTooltipToCreateFormula(editorDom: HTMLElement, convert: () => void) { function buildTooltip(ctl: ITooltipControl) { return cssConvertTooltip(icon('Convert'), - cssLink('Convert column to formula', + cssLink(t('Convert column to formula'), dom.on('mousedown', (ev) => { ev.preventDefault(); convert(); }), testId('editor-tooltip-convert'), ), diff --git a/app/client/widgets/FieldBuilder.ts b/app/client/widgets/FieldBuilder.ts index 2e07025c..a4d77ac7 100644 --- a/app/client/widgets/FieldBuilder.ts +++ b/app/client/widgets/FieldBuilder.ts @@ -9,6 +9,7 @@ import { KoArray } from 'app/client/lib/koArray'; import * as kd from 'app/client/lib/koDom'; import * as kf from 'app/client/lib/koForm'; import * as koUtil from 'app/client/lib/koUtil'; +import { makeT } from 'app/client/lib/localization'; import { reportError } from 'app/client/models/AppModel'; import { DataRowModel } from 'app/client/models/DataRowModel'; import { ColumnRec, DocModel, ViewFieldRec } from 'app/client/models/DocModel'; @@ -38,7 +39,7 @@ import * as ko from 'knockout'; import * as _ from 'underscore'; const testId = makeTestId('test-fbuilder-'); - +const t = makeT('FieldBuilder'); // Creates a FieldBuilder object for each field in viewFields @@ -228,7 +229,7 @@ export class FieldBuilder extends Disposable { defaultWidget.onWrite((value) => this.field.config.widget(value)); const disabled = Computed.create(null, use => !use(this.field.config.sameWidgets)); return [ - cssLabel('CELL FORMAT'), + cssLabel(t('CELL FORMAT')), cssRow( grainjsDom.autoDispose(defaultWidget), widgetOptions.length <= 2 ? @@ -242,7 +243,7 @@ export class FieldBuilder extends Disposable { widgetOptions, { disabled, - defaultLabel: 'Mixed format' + defaultLabel: t('Mixed format') } ), testId('widget-select') @@ -286,7 +287,7 @@ export class FieldBuilder extends Disposable { // If we are waiting for a server response use(this.isCallPending), menuCssClass: cssTypeSelectMenu.className, - defaultLabel: 'Mixed types', + defaultLabel: t('Mixed types'), renderOptionArgs: (op) => { if (['Ref', 'RefList'].includes(selectType.get())) { // Don't show tip if a reference column type is already selected. @@ -340,7 +341,7 @@ export class FieldBuilder extends Disposable { // If we selected multiple empty/formula columns, make the change for all of them. if (this.field.viewSection.peek().selectedFields.peek().length > 1 && ['formula', 'empty'].indexOf(this.field.viewSection.peek().columnsBehavior.peek())) { - return this.gristDoc.docData.bundleActions("Changing multiple column types", () => + return this.gristDoc.docData.bundleActions(t("Changing multiple column types"), () => Promise.all(this.field.viewSection.peek().selectedFields.peek().map(f => f.column.peek().type.setAndSave(calculatedType) ))).catch(reportError); @@ -369,7 +370,7 @@ export class FieldBuilder extends Disposable { return use(this.origColumn.disableModifyBase) || use(this.field.config.multiselect); }); return [ - cssLabel('DATA FROM TABLE', + cssLabel(t('DATA FROM TABLE'), !this._showRefConfigPopup.peek() ? null : this.gristDoc.behavioralPromptsManager.attachTip( 'referenceColumnsConfig', { @@ -417,7 +418,7 @@ export class FieldBuilder extends Disposable { } }), kf.row( - 15, kf.label('Apply Formula to Data'), + 15, kf.label(t('Apply Formula to Data')), 3, kf.buttonGroup( kf.checkButton(transformButton, dom('span.glyphicon.glyphicon-flash'), @@ -498,7 +499,7 @@ export class FieldBuilder extends Disposable { public fieldSettingsUseSeparate() { return this.gristDoc.docData.bundleActions( - `Use separate field settings for ${this.origColumn.colId()}`, () => { + t("Use separate field settings for {{colId}}", { colId: this.origColumn.colId() }), () => { return Promise.all([ setSaveValue(this.field.widgetOptions, this.field.column().widgetOptions()), setSaveValue(this.field.visibleCol, this.field.column().visibleCol()), @@ -510,7 +511,7 @@ export class FieldBuilder extends Disposable { public fieldSettingsSaveAsCommon() { return this.gristDoc.docData.bundleActions( - `Save field settings for ${this.origColumn.colId()} as common`, () => { + t("Save field settings for {{colId}} as common", { colId: this.origColumn.colId() }), () => { return Promise.all([ setSaveValue(this.field.column().widgetOptions, this.field.widgetOptions()), setSaveValue(this.field.column().visibleCol, this.field.visibleCol()), @@ -525,7 +526,7 @@ export class FieldBuilder extends Disposable { public fieldSettingsRevertToCommon() { return this.gristDoc.docData.bundleActions( - `Revert field settings for ${this.origColumn.colId()} to common`, () => { + t("Revert field settings for {{colId}} to common", { colId: this.origColumn.colId() }), () => { return Promise.all([ setSaveValue(this.field.widgetOptions, ''), setSaveValue(this.field.visibleCol, 0), diff --git a/app/client/widgets/FieldEditor.ts b/app/client/widgets/FieldEditor.ts index 68ced9c0..97cfb3dc 100644 --- a/app/client/widgets/FieldEditor.ts +++ b/app/client/widgets/FieldEditor.ts @@ -2,6 +2,7 @@ import * as commands from 'app/client/components/commands'; import {Cursor} from 'app/client/components/Cursor'; import {GristDoc} from 'app/client/components/GristDoc'; import {UnsavedChange} from 'app/client/components/UnsavedChanges'; +import {makeT} from 'app/client/lib/localization'; import {DataRowModel} from 'app/client/models/DataRowModel'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {reportError} from 'app/client/models/errors'; @@ -17,6 +18,8 @@ import {CellPosition} from "app/client/components/CellPosition"; type IEditorConstructor = typeof NewBaseEditor; +const t = makeT('FieldEditor'); + /** * Check if the typed-in value should change the cell without opening the cell editor, and if so, * saves and returns true. E.g. on typing space, CheckBoxEditor toggles the cell without opening. @@ -320,7 +323,7 @@ export class FieldEditor extends Disposable { await editor.prepForSave(); if (this.isDisposed()) { // We shouldn't normally get disposed here, but if we do, avoid confusing JS errors. - console.warn("Unable to finish saving edited cell"); // tslint:disable-line:no-console + console.warn(t("Unable to finish saving edited cell")); // tslint:disable-line:no-console return false; } @@ -349,7 +352,7 @@ export class FieldEditor extends Disposable { const value = editor.getCellValue(); if (col.isRealFormula()) { // tslint:disable-next-line:no-console - console.warn("It should be impossible to save a plain data value into a formula column"); + console.warn(t("It should be impossible to save a plain data value into a formula column")); } else { // This could still be an isFormula column if it's empty (isEmpty is true), but we don't // need to toggle isFormula in that case, since the data engine takes care of that. diff --git a/app/client/widgets/FormulaEditor.ts b/app/client/widgets/FormulaEditor.ts index eb57cd50..4968a572 100644 --- a/app/client/widgets/FormulaEditor.ts +++ b/app/client/widgets/FormulaEditor.ts @@ -1,5 +1,6 @@ import * as AceEditor from 'app/client/components/AceEditor'; import {createGroup} from 'app/client/components/commands'; +import {makeT} from 'app/client/lib/localization'; import {DataRowModel} from 'app/client/models/DataRowModel'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {colors, testId, theme} from 'app/client/ui2018/cssVars'; @@ -20,6 +21,7 @@ import debounce = require('lodash/debounce'); // How wide to expand the FormulaEditor when an error is shown in it. const minFormulaErrorWidth = 400; +const t = makeT('FormulaEditor'); export interface IFormulaEditorOptions extends Options { cssClass?: string; @@ -293,7 +295,7 @@ export function openFormulaEditor(options: { const column = options.column ?? options.field?.column(); if (!column) { - throw new Error('Column or field is required'); + throw new Error(t('Column or field is required')); } // AsyncOnce ensures it's called once even if triggered multiple times. @@ -338,7 +340,7 @@ export function openFormulaEditor(options: { const editingFormula = options.editingFormula ?? options?.field?.editingFormula; if (!editingFormula) { - throw new Error('editingFormula is required'); + throw new Error(t('editingFormula is required')); } // When formula is empty enter formula-editing mode (highlight formula icons; click on a column inserts its ID). @@ -393,9 +395,9 @@ export function createFormulaErrorObs(owner: MultiHolder, gristDoc: GristDoc, or const numErrors = tableData.countErrors(colId) || 0; errorMessage.set( (numErrors === 0) ? '' : - (numCells === 1) ? `Error in the cell` : - (numErrors === numCells) ? `Errors in all ${numErrors} cells` : - `Errors in ${numErrors} of ${numCells} cells` + (numCells === 1) ? t(`Error in the cell`) : + (numErrors === numCells) ? t(`Errors in all {{numErrors}} cells`, {numErrors}) : + t(`Errors in {{numErrors}} of {{numCells}} cells`, {numErrors, numCells}) ); } else { errorMessage.set(''); diff --git a/app/client/widgets/HyperLinkEditor.ts b/app/client/widgets/HyperLinkEditor.ts index ae5ab420..44aacd52 100644 --- a/app/client/widgets/HyperLinkEditor.ts +++ b/app/client/widgets/HyperLinkEditor.ts @@ -1,6 +1,9 @@ +import {makeT} from 'app/client/lib/localization'; import {FieldOptions} from 'app/client/widgets/NewBaseEditor'; import {NTextEditor} from 'app/client/widgets/NTextEditor'; +const t = makeT('HyperLinkEditor'); + /** * HyperLinkEditor - Is the same NTextEditor but with some placeholder text to help explain * to the user how links should be formatted. @@ -8,6 +11,6 @@ import {NTextEditor} from 'app/client/widgets/NTextEditor'; export class HyperLinkEditor extends NTextEditor { constructor(options: FieldOptions) { super(options); - this.textInput.setAttribute('placeholder', '[link label] url'); + this.textInput.setAttribute('placeholder', t('[link label] url')); } } diff --git a/app/client/widgets/NumericTextBox.ts b/app/client/widgets/NumericTextBox.ts index 682382fe..48c7397b 100644 --- a/app/client/widgets/NumericTextBox.ts +++ b/app/client/widgets/NumericTextBox.ts @@ -1,6 +1,7 @@ /** * See app/common/NumberFormat for description of options we support. */ +import {makeT} from 'app/client/lib/localization'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {reportError} from 'app/client/models/errors'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; @@ -16,6 +17,7 @@ import {BindableValue, Computed, dom, DomContents, DomElementArg, import * as LocaleCurrency from 'locale-currency'; +const t = makeT('NumericTextBox'); const modeOptions: Array> = [ {value: 'currency', label: '$'}, {value: 'decimal', label: ','}, @@ -85,23 +87,23 @@ export class NumericTextBox extends NTextBox { return [ super.buildConfigDom(), - cssLabel('Number Format'), + cssLabel(t('Number Format')), cssRow( dom.autoDispose(holder), makeButtonSelect(numMode, modeOptions, setMode, disabledStyle, cssModeSelect.cls(''), testId('numeric-mode')), makeButtonSelect(numSign, signOptions, setSign, disabledStyle, cssSignSelect.cls(''), testId('numeric-sign')), ), dom.maybe((use) => use(numMode) === 'currency', () => [ - cssLabel('Currency'), + cssLabel(t('Currency')), cssRow( dom.domComputed(docCurrency, (defaultCurrency) => buildCurrencyPicker(holder, currency, setCurrency, - {defaultCurrencyLabel: `Default currency (${defaultCurrency})`, disabled}) + {defaultCurrencyLabel: t(`Default currency ({{defaultCurrency}})`, {defaultCurrency}), disabled}) ), testId("numeric-currency") ) ]), - cssLabel('Decimals'), + cssLabel(t('Decimals')), cssRow( decimals('min', minDecimals, defaultMin, setMinDecimals, disabled, testId('numeric-min-decimals')), decimals('max', maxDecimals, defaultMax, setMaxDecimals, disabled, testId('numeric-max-decimals')), diff --git a/app/client/widgets/Reference.ts b/app/client/widgets/Reference.ts index 006876c7..e0cef663 100644 --- a/app/client/widgets/Reference.ts +++ b/app/client/widgets/Reference.ts @@ -1,3 +1,4 @@ +import {makeT} from 'app/client/lib/localization'; import {DataRowModel} from 'app/client/models/DataRowModel'; import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; @@ -8,6 +9,9 @@ import {NTextBox} from 'app/client/widgets/NTextBox'; import {isFullReferencingType, isVersions} from 'app/common/gristTypes'; import {Computed, dom, styled} from 'grainjs'; + +const t = makeT('Reference'); + /** * Reference - The widget for displaying references to another table's records. */ @@ -33,14 +37,14 @@ export class Reference extends NTextBox { icon: 'FieldColumn', disabled: isFullReferencingType(use(col.type)) || use(col.isTransforming) })) - .concat([{label: 'Row ID', value: 0, icon: 'FieldColumn'}]); + .concat([{label: t('Row ID'), value: 0, icon: 'FieldColumn'}]); }); } public buildConfigDom() { return [ this.buildTransformConfigDom(), - cssLabel('CELL FORMAT'), + cssLabel(t('CELL FORMAT')), super.buildConfigDom() ]; } @@ -48,7 +52,7 @@ export class Reference extends NTextBox { public buildTransformConfigDom() { const disabled = Computed.create(null, use => use(this.field.config.multiselect)); return [ - cssLabel('SHOW COLUMN'), + cssLabel(t('SHOW COLUMN')), cssRow( dom.autoDispose(disabled), select(this._visibleColRef, this._validCols, { diff --git a/app/client/widgets/UserType.ts b/app/client/widgets/UserType.ts index 601260a3..e3e2f223 100644 --- a/app/client/widgets/UserType.ts +++ b/app/client/widgets/UserType.ts @@ -36,6 +36,7 @@ export function mergeOptions(options: any, type: string) { // The names of widgets are used, instead of the actual classes needed, in order to limit // the spread of dependencies. See ./UserTypeImpl for actual classes. export const typeDefs: any = { + // TODO : translate labels (can not use classic makeT function) Any: { label: 'Any', icon: 'FieldAny', diff --git a/static/locales/de.client.json b/static/locales/de.client.json index 63316368..a1b7b526 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -666,7 +666,7 @@ "Current field ": "Aktuelles Feld ", "OK": "OK" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Anwenden", "Cancel": "Abbrechen", "Preview": "Vorschau", diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 88646e26..5b0ab597 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -27,17 +27,21 @@ "Permission to view Access Rules": "Permission to view Access Rules", "Permissions": "Permissions", "Remove column {{- colId }} from {{- tableId }} rules": "Remove column {{- colId }} from {{- tableId }} rules", - "Remove {{- tableId }} rules": "Remove {{- tableId }} rules", "Remove {{- name }} user attribute": "Remove {{- name }} user attribute", + "Remove {{- tableId }} rules": "Remove {{- tableId }} rules", "Reset": "Reset", "Rules for table ": "Rules for table ", "Save": "Save", "Saved": "Saved", + "Seed rules": "Seed rules", "Special Rules": "Special Rules", "Type a message...": "Type a message…", "User Attributes": "User Attributes", "View As": "View As", +<<<<<<< HEAD "Seed rules": "Seed rules", +======= +>>>>>>> trad: make the widgets and the Welcome Tour translatable "When adding table rules, automatically add a rule to grant OWNER full access.": "When adding table rules, automatically add a rule to grant OWNER full access." }, "AccountPage": { @@ -70,11 +74,6 @@ "Switch Accounts": "Switch Accounts", "Toggle Mobile Mode": "Toggle Mobile Mode" }, - "ViewAsDropdown": { - "View As": "View As", - "Users from table": "Users from table", - "Example Users": "Example Users" - }, "ActionLog": { "Action Log failed to load": "Action Log failed to load", "Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "Column {{colId}} was subsequently removed in action #{{action.actionNum}}", @@ -129,6 +128,13 @@ "Reset {{count}} entire columns_one": "Reset entire column", "Reset {{count}} entire columns_other": "Reset {{count}} entire columns" }, + "CellStyle": { + "CELL STYLE": "CELL STYLE", + "Cell Style": "Cell Style", + "Default cell style": "Default cell style", + "Mixed style": "Mixed style", + "Open row styles": "Open row styles" + }, "ChartView": { "Create separate series for each value of the selected column.": "Create separate series for each value of the selected column.", "Each Y series is followed by a series for the length of error bars.": "Each Y series is followed by a series for the length of error bars.", @@ -137,6 +143,9 @@ "Toggle chart aggregation": "Toggle chart aggregation", "selected new group data columns": "selected new group data columns" }, + "ChoiceTextBox": { + "CHOICES": "CHOICES" + }, "CodeEditorPanel": { "Access denied": "Access denied", "Code View is available only when you have full document access.": "Code View is available only when you have full document access." @@ -150,20 +159,30 @@ "All": "All", "All Except": "All Except", "All Shown": "All Shown", + "End": "End", "Filter by Range": "Filter by Range", "Future Values": "Future Values", + "Max": "Max", + "Min": "Min", "No matching values": "No matching values", "None": "None", - "Min": "Min", - "Max": "Max", - "Start": "Start", - "End": "End", "Other Matching": "Other Matching", "Other Non-Matching": "Other Non-Matching", "Other Values": "Other Values", "Others": "Others", "Search": "Search", - "Search values": "Search values" + "Search values": "Search values", + "Start": "Start" + }, + "ConditionalStyle": { + "Add another rule": "Add another rule", + "Add conditional style": "Add conditional style", + "Error in style rule": "Error in style rule", + "Row Style": "Row Style", + "Rule must return True or False": "Rule must return True or False" + }, + "CurrencyPicker": { + "Invalid currency": "Invalid currency" }, "CustomSectionConfig": { " (optional)": " (optional)", @@ -191,6 +210,24 @@ "Table ID copied to clipboard": "Table ID copied to clipboard", "You do not have edit access to this document": "You do not have edit access to this document" }, + "DiscussionEditor": { + "Cancel": "Cancel", + "Comment": "Comment", + "Edit": "Edit", + "Marked as resolved": "Marked as resolved", + "Only current page": "Only current page", + "Only my threads": "Only my threads", + "Open": "Open", + "Remove": "Remove", + "Reply": "Reply", + "Reply to a comment": "Reply to a comment", + "Resolve": "Resolve", + "Save": "Save", + "Show resolved comments": "Show resolved comments", + "Showing last {{nb}} comments": "Showing last {{nb}} comments", + "Started discussion": "Started discussion", + "Write a comment": "Write a comment" + }, "DocHistory": { "Activity": "Activity", "Beta": "Beta", @@ -291,6 +328,9 @@ "Name for new table": "Name for new table", "Only the document default access rules will apply to the copy.": "Only the document default access rules will apply to the copy." }, + "EditorTooltip": { + "Convert column to formula": "Convert column to formula" + }, "ExampleInfo": { "Afterschool Program": "Afterschool Program", "Check out our related tutorial for how to link data, and create high-productivity layouts.": "Check out our related tutorial for how to link data, and create high-productivity layouts.", @@ -305,6 +345,17 @@ "Welcome to the Investment Research template": "Welcome to the Investment Research template", "Welcome to the Lightweight CRM template": "Welcome to the Lightweight CRM template" }, + "FieldBuilder": { + "Apply Formula to Data": "Apply Formula to Data", + "CELL FORMAT": "CELL FORMAT", + "Changing multiple column types": "Changing multiple column types", + "DATA FROM TABLE": "DATA FROM TABLE", + "Mixed format": "Mixed format", + "Mixed types": "Mixed types", + "Revert field settings for {{colId}} to common": "Revert field settings for {{colId}} to common", + "Save field settings for {{colId}} as common": "Save field settings for {{colId}} as common", + "Use separate field settings for {{colId}}": "Use separate field settings for {{colId}}" + }, "FieldConfig": { "COLUMN BEHAVIOR": "COLUMN BEHAVIOR", "COLUMN LABEL AND ID": "COLUMN LABEL AND ID", @@ -326,6 +377,10 @@ "Set trigger formula": "Set trigger formula", "TRIGGER FORMULA": "TRIGGER FORMULA" }, + "FieldEditor": { + "It should be impossible to save a plain data value into a formula column": "It should be impossible to save a plain data value into a formula column", + "Unable to finish saving edited cell": "Unable to finish saving edited cell" + }, "FieldMenus": { "Revert to common settings": "Revert to common settings", "Save as common settings": "Save as common settings", @@ -340,6 +395,13 @@ "SearchColumns": "Search columns", "Search Columns": "Search Columns" }, + "FormulaEditor": { + "Column or field is required": "Column or field is required", + "Error in the cell": "Error in the cell", + "Errors in all {{numErrors}} cells": "Errors in all {{numErrors}} cells", + "Errors in {{numErrors}} of {{numCells}} cells": "Errors in {{numErrors}} of {{numCells}} cells", + "editingFormula is required": "editingFormula is required" + }, "GridOptions": { "Grid Options": "Grid Options", "Horizontal Gridlines": "Horizontal Gridlines", @@ -417,6 +479,9 @@ "Workspace will be moved to Trash.": "Workspace will be moved to Trash.", "Workspaces": "Workspaces" }, + "HyperLinkEditor": { + "[link label] url": "[link label] url" + }, "Importer": { "Merge rows that match these fields:": "Merge rows that match these fields:", "Select fields to match on": "Select fields to match on", @@ -437,8 +502,8 @@ "No destination workspace": "No destination workspace", "Organization": "Organization", "Original Has Modifications": "Original Has Modifications", - "Original Looks Unrelated": "Original Looks Unrelated", "Original Looks Identical": "Original Looks Identical", + "Original Looks Unrelated": "Original Looks Unrelated", "Overwrite": "Overwrite", "Replacing the original requires editing rights on the original document.": "Replacing the original requires editing rights on the original document.", "Sign up": "Sign up", @@ -461,6 +526,12 @@ "Report a problem": "Report a problem", "Upgrade Plan": "Upgrade Plan" }, + "NumericTextBox": { + "Currency": "Currency", + "Decimals": "Decimals", + "Default currency ({{defaultCurrency}})": "Default currency ({{defaultCurrency}})", + "Number Format": "Number Format" + }, "OnBoardingPopups": { "Finish": "Finish", "Next": "Next" @@ -496,15 +567,20 @@ }, "RecordLayoutEditor": { "Add Field": "Add Field", + "Cancel": "Cancel", "Create New Field": "Create New Field", - "Show field {{- label}}": "Show field {{- label}}", "Save Layout": "Save Layout", - "Cancel": "Cancel" + "Show field {{- label}}": "Show field {{- label}}" }, "RefSelect": { "Add Column": "Add Column", "No columns to add": "No columns to add" }, + "Reference": { + "CELL FORMAT": "CELL FORMAT", + "Row ID": "Row ID", + "SHOW COLUMN": "SHOW COLUMN" + }, "RightPanel": { "CHART TYPE": "CHART TYPE", "COLUMN TYPE": "COLUMN TYPE", @@ -577,9 +653,9 @@ "Add Column": "Add Column", "Empty values last": "Empty values last", "Natural sort": "Natural sort", + "Search Columns": "Search columns", "Update Data": "Update Data", - "Use choice position": "Use choice position", - "Search Columns": "Search columns" + "Use choice position": "Use choice position" }, "SortFilterConfig": { "Filter": "FILTER", @@ -618,13 +694,6 @@ "Current field ": "Current field ", "OK": "OK" }, - "TypeTransformation": { - "Apply": "Apply", - "Cancel": "Cancel", - "Preview": "Preview", - "Revise": "Revise", - "Update formula (Shift+Enter)": "Update formula (Shift+Enter)" - }, "UserManagerModel": { "Editor": "Editor", "In Full": "In Full", @@ -642,6 +711,11 @@ "ViewAsBanner": { "UnknownUser": "Unknown User" }, + "ViewAsDropdown": { + "Example Users": "Example Users", + "Users from table": "Users from table", + "View As": "View As" + }, "ViewConfigTab": { "Advanced settings": "Advanced settings", "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.": "Big tables may be marked as \"on-demand\" to avoid loading them into the data engine.", @@ -779,5 +853,31 @@ "Preview": "Preview", "Revise": "Revise", "Update formula (Shift+Enter)": "Update formula (Shift+Enter)" + }, + "welcomeTour": { + "Add New": "Add New", + "Browse our {{templateLibrary}} to discover what's possible and get inspired.": "Browse our {{templateLibrary}} to discover what's possible and get inspired.", + "Building up": "Building up", + "Configuring your document": "Configuring your document", + "Customizing columns": "Customizing columns", + "Double-click or hit {{enter}} on a cell to edit it. ": "Double-click or hit {{enter}} on a cell to edit it. ", + "Editing Data": "Editing Data", + "Enter": "Enter", + "Flying higher": "Flying higher", + "Help Center": "Help Center", + "Make it relational! Use the {{ref}} type to link tables. ": "Make it relational! Use the {{ref}} type to link tables. ", + "Reference": "Reference", + "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ": "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ", + "Share": "Share", + "Sharing": "Sharing", + "Start with {{equal}} to enter a formula.": "Start with {{equal}} to enter a formula.", + "Toggle the {{creatorPanel}} to format columns, ": "Toggle the {{creatorPanel}} to format columns, ", + "Use the Share button ({{share}}) to share the document or export data.": "Use the Share button ({{share}}) to share the document or export data.", + "Use {{addNew}} to add widgets, pages, or import more data. ": "Use {{addNew}} to add widgets, pages, or import more data. ", + "Use {{helpCenter}} for documentation or questions.": "Use {{helpCenter}} for documentation or questions.", + "Welcome to Grist!": "Welcome to Grist!", + "convert to card view, select data, and more.": "convert to card view, select data, and more.", + "creator panel": "creator panel", + "template library": "template library" } } diff --git a/static/locales/es.client.json b/static/locales/es.client.json index cae62025..6a09a427 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -768,7 +768,7 @@ "SelectionSummary": { "Copied to clipboard": "Copiado al portapapeles" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Aplicar", "Cancel": "Cancelar", "Preview": "Vista previa", diff --git a/static/locales/fr.client.json b/static/locales/fr.client.json index 21c2b7bc..15b41fe8 100644 --- a/static/locales/fr.client.json +++ b/static/locales/fr.client.json @@ -610,7 +610,7 @@ "Cancel": "Annuler", "Close": "Fermer" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Appliquer", "Cancel": "Annuler", "Preview": "Aperçu", diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index b9b53227..8f559400 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -666,7 +666,7 @@ "Current field ": "Campo atual ", "OK": "OK" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Aplicar", "Cancel": "Cancelar", "Preview": "Pré-visualização",