diff --git a/.github/workflows/translation_keys.yml b/.github/workflows/translation_keys.yml new file mode 100644 index 00000000..39bc35c7 --- /dev/null +++ b/.github/workflows/translation_keys.yml @@ -0,0 +1,61 @@ +name: Translation keys + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + pull-requests: write + contents: write + +jobs: + build: + if: github.repository_owner == 'gristlabs' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Let's get all the branches + + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 14 + + - name: Install Node.js packages + run: yarn install + + - name: Build code + run: yarn run build:prod + + - name: Scan for keys + id: scan-keys + run: | + git checkout -b translation-keys + yarn run generate:translation + git status --porcelain + if [[ $(git status --porcelain | wc -l) -eq "0" ]]; then + echo "No changes" + echo "CHANGED=false" >> $GITHUB_ENV + else + echo "Changes detected" + echo "CHANGED=true" >> $GITHUB_ENV + fi + + - name: setup git config + run: | + git config user.name "Paul's Grist Bot" + git config user.email "" + + - name: Prepare PR + if: env.CHANGED == 'true' + run: | + git commit -m "automated update to translation keys" -a + git push --set-upstream origin HEAD:translation-keys -f + num=$(gh pr list --search "automated update to translation keys" --json number -q ".[].number") + if [[ "$num" = "" ]]; then + gh pr create --fill + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/app/client/ui/RightPanelStyles.ts b/app/client/ui/RightPanelStyles.ts index 89e1e1bc..52fbb77d 100644 --- a/app/client/ui/RightPanelStyles.ts +++ b/app/client/ui/RightPanelStyles.ts @@ -22,6 +22,8 @@ export const cssHelp = styled('div', ` export const cssRow = styled('div', ` display: flex; + flex-wrap: wrap; + row-gap: 5px; margin: 8px 16px; align-items: center; color: ${theme.text}; diff --git a/app/client/ui/welcomeTour.ts b/app/client/ui/welcomeTour.ts index 2b6610f6..dea0e4ab 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,53 @@ 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/CheckBox.css b/app/client/widgets/CheckBox.css index d204b5de..1739ca02 100644 --- a/app/client/widgets/CheckBox.css +++ b/app/client/widgets/CheckBox.css @@ -6,20 +6,20 @@ height: 16px; } -.field_clip.has_cursor > .widget_checkbox { +:not(.formula_field)>.field_clip.has_cursor>.widget_checkbox { cursor: pointer; box-shadow: inset 0 0 0 1px #606060; border-radius: 3px; background: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(252,252,252,1) 29%, rgba(239,239,239,1) 50%, rgba(232,232,232,1) 50%, rgba(242,242,242,1) 100%); } -.widget_checkbox:hover:not(.disabled) { +:not(.formula_field)>.field_clip>.widget_checkbox:hover:not(.disabled) { cursor: pointer; box-shadow: inset 0 0 0 1px #606060; border-radius: 3px; background: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(252,252,252,1) 29%, rgba(239,239,239,1) 50%, rgba(232,232,232,1) 50%, rgba(242,242,242,1) 100%); } -.widget_checkbox:active:not(.disabled) { +:not(.formula_field)>.field_clip>.widget_checkbox:active:not(.disabled) { background: linear-gradient(to bottom, rgba(147,180,242,1) 0%, rgba(135,168,233,1) 10%, rgba(115,149,218,1) 25%, rgba(115,150,224,1) 37%, rgba(115,153,230,1) 50%, rgba(86,134,219,1) 51%, rgba(130,174,235,1) 83%, rgba(151,194,243,1) 100%); } 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..c806f912 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('EditorTooltip'); + 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..0f400bde 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -39,7 +39,8 @@ "Users": "Benutzer", "View As": "Anzeigen als", "Seed rules": "Saatgut-Regeln", - "When adding table rules, automatically add a rule to grant OWNER full access.": "Beim Hinzufügen von Tabellenregeln wird automatisch eine Regel hinzugefügt, um BESITZER vollen Zugriff zu gewähren." + "When adding table rules, automatically add a rule to grant OWNER full access.": "Beim Hinzufügen von Tabellenregeln wird automatisch eine Regel hinzugefügt, um BESITZER vollen Zugriff zu gewähren.", + "Permission to edit document structure": "Berechtigung zur Bearbeitung der Dokumentenstruktur" }, "AccountPage": { "API": "API", @@ -274,7 +275,10 @@ "Save": "Speichern", "Save and Reload": "Speichern und neu laden", "This document's ID (for API use):": "Die ID dieses Dokuments (für API-Verwendung):", - "Time Zone:": "Zeitzone:" + "Time Zone:": "Zeitzone:", + "API": "API", + "Document ID copied to clipboard": "Dokument-ID in die Zwischenablage kopiert", + "Ok": "Ok" }, "DocumentUsage": { "Attachments Size": "Anhänge Größe", @@ -649,7 +653,8 @@ "Return to viewing as yourself": "Zurück zur Selbstdarstellung", "TOOLS": "WERKZEUGE", "Tour of this Document": "Tour durch dieses Dokument", - "Validate Data": "Daten validieren" + "Validate Data": "Daten validieren", + "Settings": "Einstellungen" }, "TopBar": { "Manage Team": "Team verwalten" @@ -666,12 +671,12 @@ "Current field ": "Aktuelles Feld ", "OK": "OK" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Anwenden", "Cancel": "Abbrechen", "Preview": "Vorschau", - "Revise": "Überarbeiten", - "Update formula (Shift+Enter)": "Formel aktualisieren (Umschalttaste+Eingabetaste)" + "Update formula (Shift+Enter)": "Formel aktualisieren (Umschalttaste+Eingabetaste)", + "Revise": "Überarbeiten" }, "UserManagerModel": { "Editor": "Redakteur", @@ -834,11 +839,125 @@ "Users from table": "Benutzer aus der Tabelle", "View As": "Anzeigen als" }, - "TypeTransform": { - "Apply": "Anwenden", + "TypeTransformation": { + "Update formula (Shift+Enter)": "Formel aktualisieren (Umschalttaste+Eingabetaste)", "Cancel": "Abbrechen", + "Revise": "Überarbeiten", "Preview": "Vorschau", - "Update formula (Shift+Enter)": "Formel aktualisieren (Umschalttaste+Eingabetaste)", - "Revise": "Überarbeiten" + "Apply": "Anwenden" + }, + "CellStyle": { + "CELL STYLE": "ZELLENSTIL", + "Open row styles": "Zeilenstile öffnen", + "Cell Style": "Zellenstil", + "Default cell style": "Standard-Zellenstil", + "Mixed style": "Gemischter Stil" + }, + "DiscussionEditor": { + "Resolve": "Beschließen", + "Save": "Speichern", + "Show resolved comments": "Gelöste Kommentare anzeigen", + "Only my threads": "Nur meine Fäden", + "Open": "Öffnen", + "Remove": "Entfernen", + "Reply to a comment": "Auf einen Kommentar antworten", + "Reply": "Antwort", + "Comment": "Kommentar", + "Edit": "Bearbeiten", + "Only current page": "Nur aktuelle Seite", + "Started discussion": "Begonnene Diskussion", + "Write a comment": "Schreiben Sie einen Kommentar", + "Marked as resolved": "Markiert als gelöst", + "Cancel": "Abbrechen", + "Showing last {{nb}} comments": "Letzte {{nb}} Kommentare anzeigen" + }, + "ColumnInfo": { + "COLUMN DESCRIPTION": "SPALTENBESCHREIBUNG", + "COLUMN ID: ": "SPALTEN-ID: ", + "Save": "Speichern", + "COLUMN LABEL": "SPALTENBEZEICHNUNG", + "Cancel": "Abbrechen" + }, + "ChoiceTextBox": { + "CHOICES": "WAHLEN" + }, + "ColumnEditor": { + "COLUMN DESCRIPTION": "SPALTENBESCHREIBUNG", + "COLUMN LABEL": "SPALTENBEZEICHNUNG" + }, + "ConditionalStyle": { + "Add conditional style": "Bedingten Stil hinzufügen", + "Error in style rule": "Fehler in der Stilregel", + "Rule must return True or False": "Regel muss wahr oder falsch zurückgeben", + "Add another rule": "Eine weitere Regel hinzufügen", + "Row Style": "Zeilenstil" + }, + "CurrencyPicker": { + "Invalid currency": "Ungültige Währung" + }, + "FieldBuilder": { + "Changing multiple column types": "Mehrere Spaltentypen ändern", + "Mixed format": "Gemischtes Format", + "Mixed types": "Gemischte Typen", + "Revert field settings for {{colId}} to common": "Feldeinstellungen für {{colId}} auf \"Allgemein\" zurücksetzen", + "Apply Formula to Data": "Formel auf Daten anwenden", + "CELL FORMAT": "ZELLENFORMAT", + "DATA FROM TABLE": "DATEN AUS TABELLE", + "Save field settings for {{colId}} as common": "Feldeinstellungen für {{colId}} als Algemein speichern", + "Use separate field settings for {{colId}}": "Verwenden Sie separate Feldeinstellungen für {{colId}}" + }, + "EditorTooltip": { + "Convert column to formula": "Spalte in Formel umwandeln" + }, + "FormulaEditor": { + "Column or field is required": "Spalte oder Feld ist erforderlich", + "Error in the cell": "Fehler in der Zelle", + "Errors in all {{numErrors}} cells": "Fehler in allen {{numErrors}} Zellen", + "editingFormula is required": "Bearbeitungsformel ist erforderlich", + "Errors in {{numErrors}} of {{numCells}} cells": "Fehler in {{numErrors}} von {{numCells}} Zellen" + }, + "Reference": { + "SHOW COLUMN": "SPALTE ANZEIGEN", + "CELL FORMAT": "ZELLENFORMAT", + "Row ID": "Zeilen-ID" + }, + "HyperLinkEditor": { + "[link label] url": "[Linklabel]-URL" + }, + "welcomeTour": { + "Add New": "Neu hinzufügen", + "Building up": "Zubauend", + "Configuring your document": "Konfigurieren Ihres Dokuments", + "Help Center": "Hilfe-Center", + "Sharing": "Teilen", + "Welcome to Grist!": "Willkommen bei Grist!", + "Enter": "Eingabe", + "Customizing columns": "Anpassen von Spalten", + "Browse our {{templateLibrary}} to discover what's possible and get inspired.": "Durchsuchen Sie unsere {{templateLibrary}}, um zu entdecken, was möglich ist und sich inspirieren lassen.", + "Double-click or hit {{enter}} on a cell to edit it. ": "Doppelklicken oder {{enter}} auf eine Zelle drücken, um sie zu bearbeiten. ", + "Editing Data": "Bearbeiten von Daten", + "Flying higher": "Höher fliegen", + "Share": "Teilen", + "Reference": "Referenz", + "Make it relational! Use the {{ref}} type to link tables. ": "Machen Sie es relativ! Verwenden Sie den {{ref}}-Typ, um Tabellen zu verlinken. ", + "Start with {{equal}} to enter a formula.": "Beginnen Sie mit {{equal}}, um eine Formel einzugeben.", + "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ": "Legen Sie Formatierungsoptionen, Formeln oder Spaltentypen fest, z. B. Daten, Auswahlmöglichkeiten oder Anhänge. ", + "Toggle the {{creatorPanel}} to format columns, ": "Schalten Sie die {{creatorPanel}} an, um Spalten zu formatieren, ", + "Use the Share button ({{share}}) to share the document or export data.": "Verwenden Sie die Schaltfläche Teilen ({{share}}), um das Dokument zu teilen oder Daten zu exportieren.", + "Use {{addNew}} to add widgets, pages, or import more data. ": "Verwenden Sie {{addNew}}, um Widgets und Seiten hinzuzufügen oder weitere Daten zu importieren. ", + "Use {{helpCenter}} for documentation or questions.": "Verwenden Sie {{helpCenter}} für Dokumentation oder Fragen.", + "convert to card view, select data, and more.": "in die Kartenansicht konvertieren, Daten auswählen und vieles mehr.", + "creator panel": "Ersteller-Panel", + "template library": "Vorlagenbibliothek" + }, + "NumericTextBox": { + "Currency": "Währung", + "Decimals": "Dezimalstellen", + "Default currency ({{defaultCurrency}})": "Standardwährung ({{defaultCurrency}})", + "Number Format": "Zahlenformat" + }, + "FieldEditor": { + "It should be impossible to save a plain data value into a formula column": "Es sollte unmöglich sein, einen einfachen Datenwert in eine Formelspalte zu speichern", + "Unable to finish saving edited cell": "Speichern der bearbeiteten Zelle kann nicht abgeschlossen werden" } } diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 88646e26..ce14ca56 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -38,7 +38,8 @@ "User Attributes": "User Attributes", "View As": "View As", "Seed rules": "Seed rules", - "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." + "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.", + "Permission to edit document structure": "Permission to edit document structure" }, "AccountPage": { "API": "API", @@ -603,7 +604,8 @@ "Return to viewing as yourself": "Return to viewing as yourself", "TOOLS": "TOOLS", "Tour of this Document": "Tour of this Document", - "Validate Data": "Validate Data" + "Validate Data": "Validate Data", + "Settings": "Settings" }, "TopBar": { "Manage Team": "Manage Team" @@ -779,5 +781,119 @@ "Preview": "Preview", "Revise": "Revise", "Update formula (Shift+Enter)": "Update formula (Shift+Enter)" + }, + "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" + }, + "ChoiceTextBox": { + "CHOICES": "CHOICES" + }, + "ColumnEditor": { + "COLUMN DESCRIPTION": "COLUMN DESCRIPTION", + "COLUMN LABEL": "COLUMN LABEL" + }, + "ColumnInfo": { + "COLUMN DESCRIPTION": "COLUMN DESCRIPTION", + "COLUMN ID: ": "COLUMN ID: ", + "COLUMN LABEL": "COLUMN LABEL", + "Cancel": "Cancel", + "Save": "Save" + }, + "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" + }, + "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" + }, + "EditorTooltip": { + "Convert column to formula": "Convert column to formula" + }, + "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}}" + }, + "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" + }, + "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" + }, + "HyperLinkEditor": { + "[link label] url": "[link label] url" + }, + "NumericTextBox": { + "Currency": "Currency", + "Decimals": "Decimals", + "Default currency ({{defaultCurrency}})": "Default currency ({{defaultCurrency}})", + "Number Format": "Number Format" + }, + "Reference": { + "CELL FORMAT": "CELL FORMAT", + "Row ID": "Row ID", + "SHOW COLUMN": "SHOW COLUMN" + }, + "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..fd78adb8 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -34,7 +34,8 @@ "Special Rules": "Reglas especiales", "View As": "Ver como", "Seed rules": "Reglas de semillas", - "When adding table rules, automatically add a rule to grant OWNER full access.": "Al agregar reglas de tabla, agregue automáticamente una regla para otorgar acceso completo al PROPIETARIO." + "When adding table rules, automatically add a rule to grant OWNER full access.": "Al agregar reglas de tabla, agregue automáticamente una regla para otorgar acceso completo al PROPIETARIO.", + "Permission to edit document structure": "Permiso para editar la estructura del documento" }, "AccountPage": { "API": "API", @@ -223,7 +224,10 @@ "Save": "Guardar", "Save and Reload": "Guardar y recargar", "This document's ID (for API use):": "ID de este documento (para uso de API):", - "Time Zone:": "Zona horaria:" + "Time Zone:": "Zona horaria:", + "Ok": "Ok", + "Document ID copied to clipboard": "ID de documento copiado al portapapeles", + "API": "API" }, "DuplicateTable": { "Copy all data in addition to the table structure.": "Copiar todos los datos además de la estructura de la tabla.", @@ -543,7 +547,8 @@ "Return to viewing as yourself": "Volver a ver como usted mismo", "TOOLS": "HERRAMIENTAS", "Tour of this Document": "Recorrido por este documento", - "Validate Data": "Validar datos" + "Validate Data": "Validar datos", + "Settings": "Ajustes" }, "TopBar": { "Manage Team": "Administrar equipo" @@ -768,12 +773,12 @@ "SelectionSummary": { "Copied to clipboard": "Copiado al portapapeles" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Aplicar", "Cancel": "Cancelar", - "Preview": "Vista previa", "Revise": "Revisar", - "Update formula (Shift+Enter)": "Actualizar fórmula (Mayús+Intro)" + "Update formula (Shift+Enter)": "Actualizar fórmula (Mayús+Intro)", + "Preview": "Vista previa" }, "UserManagerModel": { "Editor": "Editor", @@ -824,11 +829,125 @@ "View As": "Ver como", "Example Users": "Usuarios de ejemplo" }, - "TypeTransform": { - "Apply": "Aplicar", + "TypeTransformation": { + "Update formula (Shift+Enter)": "Actualizar fórmula (Mayús+Intro)", "Cancel": "Cancelar", "Revise": "Revisar", - "Update formula (Shift+Enter)": "Actualizar fórmula (Mayús+Intro)", + "Apply": "Aplicar", "Preview": "Vista previa" + }, + "CellStyle": { + "Cell Style": "Estilo de celda", + "Default cell style": "Estilo de celda predeterminado", + "CELL STYLE": "ESTILO DE CELDA", + "Open row styles": "Abrir estilos de fila", + "Mixed style": "Estilo mixto" + }, + "ColumnInfo": { + "Cancel": "Cancelar", + "Save": "Guardar", + "COLUMN DESCRIPTION": "DESCRIPCIÓN DE LA COLUMNA", + "COLUMN ID: ": "ID DE COLUMNA: ", + "COLUMN LABEL": "ETIQUETA DE COLUMNA" + }, + "FieldBuilder": { + "Revert field settings for {{colId}} to common": "Revertir la configuración de campo de {{colId}} a común", + "Use separate field settings for {{colId}}": "Utilice configuraciones de campo separadas para {{colId}}", + "Save field settings for {{colId}} as common": "Guardar configuración de campo de {{colId}} como común", + "DATA FROM TABLE": "DATOS DE LA TABLA", + "Mixed types": "Tipos mixtos", + "Apply Formula to Data": "Aplicar Fórmula a Datos", + "CELL FORMAT": "FORMATO DE CELDA", + "Changing multiple column types": "Cambiar varios tipos de columna", + "Mixed format": "Formato mixto" + }, + "CurrencyPicker": { + "Invalid currency": "Moneda inválida" + }, + "DiscussionEditor": { + "Cancel": "Cancelar", + "Edit": "Editar", + "Only current page": "Sólo página actual", + "Remove": "Quitar", + "Resolve": "Resolver", + "Save": "Guardar", + "Started discussion": "Discusión iniciada", + "Show resolved comments": "Mostrar comentarios resueltos", + "Write a comment": "Escribe un comentario", + "Marked as resolved": "Marcado como resuelto", + "Only my threads": "Sólo mis hilos", + "Reply": "Respuesta", + "Comment": "Comentario", + "Open": "Abrir", + "Reply to a comment": "Responder a un comentario", + "Showing last {{nb}} comments": "Mostrando los últimos {{nb}} comentarios" + }, + "EditorTooltip": { + "Convert column to formula": "Convertir columna en fórmula" + }, + "FormulaEditor": { + "Errors in {{numErrors}} of {{numCells}} cells": "Errores en {{numErrors}} de {{numCells}} celdas", + "Column or field is required": "Se requiere columna o campo", + "Error in the cell": "Error en la celda", + "Errors in all {{numErrors}} cells": "Errores en todas las {{numErrors}} celdas", + "editingFormula is required": "ediciónFórmula es necesaria" + }, + "welcomeTour": { + "Add New": "Agregar Nuevo", + "Configuring your document": "Configurando tu documento", + "Enter": "Intro", + "Flying higher": "Volando más alto", + "Reference": "Referencia", + "Start with {{equal}} to enter a formula.": "Comience con {{equal}} para introducir una fórmula.", + "Toggle the {{creatorPanel}} to format columns, ": "Active {{creatorPanel}} para dar formato a las columnas, ", + "Welcome to Grist!": "¡Bienvenido a Grist!", + "template library": "biblioteca de plantillas", + "creator panel": "panel creador", + "Sharing": "Compartiendo", + "Help Center": "Centro de ayuda", + "Building up": "Construyendo", + "Customizing columns": "Personalizando columnas", + "Double-click or hit {{enter}} on a cell to edit it. ": "Haga doble clic o pulse {{enter}} en una celda para editarla. ", + "Browse our {{templateLibrary}} to discover what's possible and get inspired.": "Explore nuestro {{templateLibrary}} para descubrir lo que es posible y sentirse inspirado.", + "Editing Data": "Editando datos", + "Make it relational! Use the {{ref}} type to link tables. ": "¡Hazlo relacional! Utilice el tipo {{ref}} para vincular tablas. ", + "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ": "Establezca opciones de formato, fórmulas o tipos de columnas, como fechas, opciones o anexos. ", + "Share": "Compartir", + "Use the Share button ({{share}}) to share the document or export data.": "Utilice el botón Compartir ({{share}}) para compartir el documento o exportar los datos.", + "Use {{helpCenter}} for documentation or questions.": "Utilice {{helpCenter}} para documentación o preguntas.", + "Use {{addNew}} to add widgets, pages, or import more data. ": "Utilice {{addNew}} para añadir widgets, páginas o importar más datos. ", + "convert to card view, select data, and more.": "convertir a la vista de la tarjeta, seleccionar datos y más." + }, + "HyperLinkEditor": { + "[link label] url": "[etiqueta de enlace] url" + }, + "NumericTextBox": { + "Currency": "Moneda", + "Default currency ({{defaultCurrency}})": "Moneda predeterminada ({{defaultCurrency}})", + "Number Format": "Formato de número", + "Decimals": "Decimales" + }, + "ChoiceTextBox": { + "CHOICES": "OPCIONES" + }, + "ColumnEditor": { + "COLUMN DESCRIPTION": "DESCRIPCIÓN DE LA COLUMNA", + "COLUMN LABEL": "ETIQUETA DE COLUMNA" + }, + "FieldEditor": { + "Unable to finish saving edited cell": "No se puede terminar de guardar la celda editada", + "It should be impossible to save a plain data value into a formula column": "Debería ser imposible guardar un valor de datos plano en una columna de fórmulas" + }, + "ConditionalStyle": { + "Add another rule": "Añadir otra regla", + "Error in style rule": "Error en la regla de estilo", + "Rule must return True or False": "La regla debe regresar Verdadera o Falsa", + "Add conditional style": "Añadir estilo condicional", + "Row Style": "Estilo de fila" + }, + "Reference": { + "SHOW COLUMN": "MOSTRAR COLUMNA", + "CELL FORMAT": "FORMATO DE CELDA", + "Row ID": "ID de fila" } } 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..64bd30ba 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -39,7 +39,8 @@ "Users": "Usuários", "View As": "Ver como", "Seed rules": "Regras de propagação", - "When adding table rules, automatically add a rule to grant OWNER full access.": "Ao adicionar regras de tabela, adicione automaticamente uma regra para conceder ao PROPRIETÁRIO acesso total." + "When adding table rules, automatically add a rule to grant OWNER full access.": "Ao adicionar regras de tabela, adicione automaticamente uma regra para conceder ao PROPRIETÁRIO acesso total.", + "Permission to edit document structure": "Permissão para editar a estrutura do documento" }, "AccountPage": { "API": "API", @@ -274,7 +275,10 @@ "Save": "Salvar", "Save and Reload": "Salvar e Recarregar", "This document's ID (for API use):": "O ID deste documento (para uso em API):", - "Time Zone:": "Fuso horário:" + "Time Zone:": "Fuso horário:", + "Ok": "Ok", + "Document ID copied to clipboard": "ID do documento copiado para a área de transferência", + "API": "API" }, "DocumentUsage": { "Attachments Size": "Tamanho dos Anexos", @@ -649,7 +653,8 @@ "Return to viewing as yourself": "Voltar a ver como você mesmo", "TOOLS": "FERRAMENTAS", "Tour of this Document": "Tour desse Documento", - "Validate Data": "Validar dados" + "Validate Data": "Validar dados", + "Settings": "Configurações" }, "TopBar": { "Manage Team": "Gerenciar Equipe" @@ -666,7 +671,7 @@ "Current field ": "Campo atual ", "OK": "OK" }, - "TypeTransformation": { + "TypeTransform": { "Apply": "Aplicar", "Cancel": "Cancelar", "Preview": "Pré-visualização", @@ -834,11 +839,125 @@ "View As": "Ver como", "Example Users": "Usuários de exemplo" }, - "TypeTransform": { - "Apply": "Aplicar", + "welcomeTour": { + "Use {{helpCenter}} for documentation or questions.": "Use {{helpCenter}} para documentação ou perguntas.", + "convert to card view, select data, and more.": "converta para a visualização de cartão, selecione dados e muito mais.", + "Start with {{equal}} to enter a formula.": "Comece com {{equal}} para inserir uma fórmula.", + "Welcome to Grist!": "Bem-vindo ao Grist!", + "template library": "biblioteca de modelos", + "Flying higher": "Voando mais alto", + "Add New": "Adicionar Novo", + "Building up": "Construindo", + "Customizing columns": "Personalizando colunas", + "Configuring your document": "Configurando seu documento", + "Make it relational! Use the {{ref}} type to link tables. ": "Faça-o relacional! Use o tipo {{ref}} para vincular tabelas. ", + "Browse our {{templateLibrary}} to discover what's possible and get inspired.": "Procure nosso {{templateLibrary}} para descobrir o que é possível e se inspirar.", + "Double-click or hit {{enter}} on a cell to edit it. ": "Clique duas vezes ou pressione {{enter}} em uma célula para editá-la. ", + "Editing Data": "Editando dados", + "Enter": "Entra", + "Help Center": "Centro de Ajuda", + "Reference": "Referência", + "Set formatting options, formulas, or column types, such as dates, choices, or attachments. ": "Defina opções de formatação, fórmulas ou tipos de coluna, como datas, escolhas ou anexos. ", + "Share": "Compartilhar", + "Sharing": "Compartilhando", + "Toggle the {{creatorPanel}} to format columns, ": "Alternar o {{creatorPanel}} para formatar colunas, ", + "Use {{addNew}} to add widgets, pages, or import more data. ": "Use {{addNew}} para adicionar widgets, páginas ou importar mais dados. ", + "creator panel": "painel do criador", + "Use the Share button ({{share}}) to share the document or export data.": "Use o botão Compartilhar ({{share}}) para compartilhar o documento ou exportar dados." + }, + "ColumnInfo": { + "COLUMN LABEL": "RÓTULO DA COLUNA", "Cancel": "Cancelar", - "Preview": "Pré-visualização", + "Save": "Salvar", + "COLUMN DESCRIPTION": "DESCRIÇÃO DA COLUNA", + "COLUMN ID: ": "ID DA COLUNA: " + }, + "ConditionalStyle": { + "Row Style": "Estilo de Linha", + "Rule must return True or False": "A regra deve retornar Verdadeiro ou Falso", + "Error in style rule": "Erro na regra de estilo", + "Add another rule": "Adicionar outra regra", + "Add conditional style": "Adicionar estilo condicional" + }, + "CurrencyPicker": { + "Invalid currency": "Moeda inválida" + }, + "DiscussionEditor": { + "Cancel": "Cancelar", + "Marked as resolved": "Marcado como resolvido", + "Reply": "Responder", + "Reply to a comment": "Responder a um comentário", + "Write a comment": "Escreva um comentário", + "Comment": "Comentário", + "Resolve": "Resolver", + "Started discussion": "Discussão iniciada", + "Edit": "Editar", + "Only current page": "Somente a página atual", + "Only my threads": "Somente meus tópicos", + "Showing last {{nb}} comments": "Mostrar os últimos {{nb}} comentários", + "Open": "Abrir", + "Remove": "Remover", + "Save": "Salvar", + "Show resolved comments": "Mostrar comentários resolvidos" + }, + "FieldBuilder": { + "CELL FORMAT": "FORMATO DA CÉLULA", + "DATA FROM TABLE": "DADOS DA TABELA", + "Apply Formula to Data": "Aplicar fórmula aos dados", + "Changing multiple column types": "Alterar vários tipos de colunas", + "Mixed format": "Formato misto", + "Save field settings for {{colId}} as common": "Salvar configurações de campo da {{colId}} como comum", + "Revert field settings for {{colId}} to common": "Reverter configurações de campo da {{colId}} para comum", + "Mixed types": "Tipos mistos", + "Use separate field settings for {{colId}}": "Use configurações de campo separadas para {{colId}}" + }, + "FormulaEditor": { + "Column or field is required": "Coluna ou campo é obrigatório", + "editingFormula is required": "ediçãoFórmula é obrigatório", + "Errors in all {{numErrors}} cells": "Erro em todas as {{numErrors}} células", + "Errors in {{numErrors}} of {{numCells}} cells": "Erros em {{numErrors}} de {{numCells}} células", + "Error in the cell": "Erro na célula" + }, + "HyperLinkEditor": { + "[link label] url": "[rótulo do link] url" + }, + "NumericTextBox": { + "Currency": "Moeda", + "Decimals": "Decimais", + "Default currency ({{defaultCurrency}})": "Moeda padrão ({{defaultCurrency}})", + "Number Format": "Formato de número" + }, + "TypeTransformation": { + "Preview": "Prévisualizar", + "Apply": "Aplicar", "Revise": "Revisar", + "Cancel": "Cancelar", "Update formula (Shift+Enter)": "Atualizar a fórmula (Shift+Enter)" + }, + "CellStyle": { + "CELL STYLE": "ESTILO DE CÉLULA", + "Cell Style": "Estilo de célula", + "Default cell style": "Estilo de célula padrão", + "Mixed style": "Estilo misto", + "Open row styles": "Estilos de linha aberta" + }, + "ColumnEditor": { + "COLUMN DESCRIPTION": "DESCRIÇÃO DA COLUNA", + "COLUMN LABEL": "RÓTULO DA COLUNA" + }, + "FieldEditor": { + "Unable to finish saving edited cell": "Não é possível concluir o salvamento da célula editada", + "It should be impossible to save a plain data value into a formula column": "Deveria ser impossível salvar um valor de dados simples em uma coluna de fórmula" + }, + "EditorTooltip": { + "Convert column to formula": "Converter coluna em fórmula" + }, + "Reference": { + "Row ID": "ID da linha", + "CELL FORMAT": "FORMATO DA CÉLULA", + "SHOW COLUMN": "MOSTRAR COLUNA" + }, + "ChoiceTextBox": { + "CHOICES": "ESCOLHAS" } }