i18n: userManager translation + some forgotten translations (#557)

* translation: add userManager translation + some forgotten translations
* use '\' caracter for multiple-line strings
This commit is contained in:
CamilleLegeron 2023-07-16 18:52:13 +02:00 committed by GitHub
parent b4b0c805ff
commit 61bd064f73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 174 additions and 101 deletions

View File

@ -1178,8 +1178,8 @@ Useful for examples and templates, but not for sensitive data.`),
}, },
SchemaEdit: { SchemaEdit: {
name: t("Permission to edit document structure"), name: t("Permission to edit document structure"),
description: t("Allow editors to edit structure (e.g. modify and delete tables, columns, " + description: t("Allow editors to edit structure (e.g. modify and delete tables, columns, \
"layouts), and to write formulas, which give access to all data regardless of read restrictions."), layouts), and to write formulas, which give access to all data regardless of read restrictions."),
availableBits: ['schemaEdit'], availableBits: ['schemaEdit'],
...schemaEditRules.denyEditors, ...schemaEditRules.denyEditors,
}, },
@ -1323,7 +1323,7 @@ class SpecialSchemaObsRuleSet extends SpecialObsRuleSet {
return dom.maybe( return dom.maybe(
(use) => use(this._body).every(rule => rule.isBuiltInOrEmpty(use)), (use) => use(this._body).every(rule => rule.isBuiltInOrEmpty(use)),
() => cssConditionError({style: 'margin-left: 56px; margin-bottom: 8px;'}, () => cssConditionError({style: 'margin-left: 56px; margin-bottom: 8px;'},
"This default should be changed if editors' access is to be limited. ", t("This default should be changed if editors' access is to be limited. "),
dom('a', {style: 'color: inherit; text-decoration: underline'}, dom('a', {style: 'color: inherit; text-decoration: underline'},
'Dismiss', dom.on('click', () => this._allowEditors('confirm'))), 'Dismiss', dom.on('click', () => this._allowEditors('confirm'))),
testId('rule-schema-edit-warning'), testId('rule-schema-edit-warning'),

View File

@ -660,8 +660,9 @@ export class ChartConfig extends GrainJSDisposable {
), ),
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) => dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
value === 'symmetric' ? cssRowHelp(t("Each Y series is followed by a series for the length of error bars.")) : value === 'symmetric' ? cssRowHelp(t("Each Y series is followed by a series for the length of error bars.")) :
value === 'separate' ? cssRowHelp(t("Each Y series is followed by two series, for " + value === 'separate' ? cssRowHelp(
"top and bottom error bars.")) t("Each Y series is followed by two series, for top and bottom error bars.")
)
: null : null
), ),
]), ]),

View File

@ -265,10 +265,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
{ {
explanation: ( explanation: (
isDocOwner isDocOwner
? t("You can try reloading the document, or using recovery mode. " + ? t("You can try reloading the document, or using recovery mode. \
"Recovery mode opens the document to be fully accessible to " + Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. \
"owners, and inaccessible to others. It also disables " + It also disables formulas. [{{error}}]", {error: err.message})
"formulas. [{{error}}]", {error: err.message})
: isDenied : isDenied
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message}) ? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message}) : t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message})

View File

@ -12,6 +12,9 @@ import {TableData} from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter'; import {BaseFormatter} from 'app/common/ValueFormatter';
import {Computed, Disposable, Observable} from 'grainjs'; import {Computed, Disposable, Observable} from 'grainjs';
import debounce = require('lodash/debounce'); import debounce = require('lodash/debounce');
import { makeT } from 'app/client/lib/localization';
const t = makeT('SearchModel');
/** /**
* SearchModel used to maintain the state of the search UI. * SearchModel used to maintain the state of the search UI.
@ -201,7 +204,7 @@ class FinderImpl implements IFinder {
// sort in order that is the same as on the raw data list page, // sort in order that is the same as on the raw data list page,
.sort((a, b) => nativeCompare(a.tableNameDef.peek(), b.tableNameDef.peek())) .sort((a, b) => nativeCompare(a.tableNameDef.peek(), b.tableNameDef.peek()))
// get rawViewSection, // get rawViewSection,
.map(t => t.rawViewSection.peek()) .map(table => table.rawViewSection.peek())
// and test if it isn't an empty record. // and test if it isn't an empty record.
.filter(s => Boolean(s.id.peek())); .filter(s => Boolean(s.id.peek()));
// Pretend that those are pages. // Pretend that those are pages.
@ -218,7 +221,7 @@ class FinderImpl implements IFinder {
// Else read all visible pages. // Else read all visible pages.
const pages = this._gristDoc.docModel.visibleDocPages.peek(); const pages = this._gristDoc.docModel.visibleDocPages.peek();
this._pageStepper.array = pages.map(p => new PageRecWrapper(p, this._openDocPageCB)); this._pageStepper.array = pages.map(p => new PageRecWrapper(p, this._openDocPageCB));
this._pageStepper.index = pages.findIndex(t => t.viewRef.peek() === this._gristDoc.activeViewId.get()); this._pageStepper.index = pages.findIndex(page => page.viewRef.peek() === this._gristDoc.activeViewId.get());
if (this._pageStepper.index < 0) { return false; } if (this._pageStepper.index < 0) { return false; }
} }
@ -468,7 +471,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } })); this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } }));
this.allLabel = Computed.create(this, use => use(this._gristDoc.activeViewId) === 'data' ? this.allLabel = Computed.create(this, use => use(this._gristDoc.activeViewId) === 'data' ?
'Search all tables' : 'Search all pages'); t('Search all tables') : t('Search all pages'));
// Schedule a search restart when user changes pages (otherwise search would resume from the // Schedule a search restart when user changes pages (otherwise search would resume from the
// previous page that is not shown anymore). Also revert noMatch flag when in single page mode. // previous page that is not shown anymore). Also revert noMatch flag when in single page mode.

View File

@ -131,9 +131,8 @@ export class AccountPage extends Disposable {
), ),
css.subHeader(t("Two-factor authentication")), css.subHeader(t("Two-factor authentication")),
css.description( css.description(
t("Two-factor authentication is an extra layer of security for your Grist account " + t("Two-factor authentication is an extra layer of security for your Grist account \
"designed to ensure that you're the only person who can access your account, " + designed to ensure that you're the only person who can access your account, even if someone knows your password.")
"even if someone knows your password.")
), ),
dom.create(MFAConfig, user), dom.create(MFAConfig, user),
), ),

View File

@ -78,8 +78,8 @@ export class ApiKey extends Disposable {
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [ dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'), basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'),
dom.boolAttr('disabled', this._loading)), dom.boolAttr('disabled', this._loading)),
description(t("By generating an API key, you will be able to " + description(t("By generating an API key, you will be able to \
"make API calls for your own account."), testId('description')), make API calls for your own account."), testId('description')),
]), ]),
); );
} }
@ -117,8 +117,8 @@ export class ApiKey extends Disposable {
() => this._onDelete(), () => this._onDelete(),
{ {
explanation: t( explanation: t(
"You're about to delete an API key. This will cause all future requests " + "You're about to delete an API key. This will cause all future requests \
"using this API key to be rejected. Do you still want to delete?" using this API key to be rejected. Do you still want to delete?"
), ),
} }
); );

View File

@ -21,8 +21,8 @@ export async function startDocTour(docData: DocData, docComm: DocComm, onFinishC
const invalidDocTour: IOnBoardingMsg[] = [{ const invalidDocTour: IOnBoardingMsg[] = [{
title: t("No valid document tour"), title: t("No valid document tour"),
body: t("Cannot construct a document tour from the data in this document. " + body: t("Cannot construct a document tour from the data in this document. \
"Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."), Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
selector: 'document', selector: 'document',
showHasModal: true, showHasModal: true,
}]; }];

View File

@ -36,8 +36,8 @@ export const buildExamples = (): IExampleInfo[] => [{
tutorialUrl: 'https://support.getgrist.com/investment-research/', tutorialUrl: 'https://support.getgrist.com/investment-research/',
welcomeCard: { welcomeCard: {
title: t("Welcome to the Investment Research template"), title: t("Welcome to the Investment Research template"),
text: t("Check out our related tutorial to learn how to create " + text: t("Check out our related tutorial to learn how to create \
"summary tables and charts, and to link charts dynamically."), summary tables and charts, and to link charts dynamically."),
tutorialName: t("Tutorial: Analyze & Visualize"), tutorialName: t("Tutorial: Analyze & Visualize"),
}, },
}, { }, {

View File

@ -50,8 +50,7 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
t('Formulas that trigger in certain cases, and store the calculated value as data.') t('Formulas that trigger in certain cases, and store the calculated value as data.')
), ),
dom('div', dom('div',
t('Useful for storing the timestamp or author of a new record, data cleaning, and ' t('Useful for storing the timestamp or author of a new record, data cleaning, and more.')
+ 'more.')
), ),
dom('div', dom('div',
cssLink({href: commonUrls.helpTriggerFormulas, target: '_blank'}, t('Learn more.')), cssLink({href: commonUrls.helpTriggerFormulas, target: '_blank'}, t('Learn more.')),
@ -76,8 +75,8 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
), ),
openAccessRules: (...args: DomElementArg[]) => cssTooltipContent( openAccessRules: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', dom('div',
t('Access rules give you the power to create nuanced rules to determine who can ' t('Access rules give you the power to create nuanced rules to determine who can \
+ 'see or edit which parts of your document.') see or edit which parts of your document.')
), ),
dom('div', dom('div',
cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.')), cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.')),
@ -126,8 +125,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
title: () => t('Reference Columns'), title: () => t('Reference Columns'),
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Select the table to link to.')), dom('div', t('Select the table to link to.')),
dom('div', t('Cells in a reference column always identify an {{entire}} ' + dom('div', t('Cells in a reference column always identify an {{entire}} \
'record in that table, but you may select which column from that record to show.', { record in that table, but you may select which column from that record to show.', {
entire: cssItalicizedText(t('entire')) entire: cssItalicizedText(t('entire'))
})), })),
dom('div', dom('div',
@ -140,8 +139,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
rawDataPage: { rawDataPage: {
title: () => t('Raw Data page'), title: () => t('Raw Data page'),
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('The Raw Data page lists all data tables in your document, ' dom('div', t('The Raw Data page lists all data tables in your document, \
+ 'including summary tables and tables not included in page layouts.')), including summary tables and tables not included in page layouts.')),
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))), dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
...args, ...args,
), ),
@ -150,8 +149,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
accessRules: { accessRules: {
title: () => t('Access Rules'), title: () => t('Access Rules'),
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Access rules give you the power to create nuanced rules ' dom('div', t('Access rules give you the power to create nuanced rules \
+ 'to determine who can see or edit which parts of your document.')), to determine who can see or edit which parts of your document.')),
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))), dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
...args, ...args,
), ),
@ -209,8 +208,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
addNew: { addNew: {
title: () => t('Add New'), title: () => t('Add New'),
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Click the Add New button to create new documents or workspaces, ' dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')),
+ 'or import data.')),
...args, ...args,
), ),
deploymentTypes: ['saas'], deploymentTypes: ['saas'],
@ -219,8 +217,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
title: () => t('Anchor Links'), title: () => t('Anchor Links'),
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', dom('div',
t('To make an anchor link that takes the user to a specific cell, click on' t('To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.',
+ ' a row and press {{shortcut}}.',
{ {
shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])), shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])),
} }
@ -235,8 +232,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
content: (...args: DomElementArg[]) => cssTooltipContent( content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', dom('div',
t( t(
'You can choose one of our pre-made widgets or embed your own ' + 'You can choose one of our pre-made widgets or embed your own by providing its full URL.'
'by providing its full URL.'
), ),
), ),
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))), dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),

View File

@ -41,8 +41,8 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a
if (cmp.summary === 'left' || cmp.summary === 'both') { if (cmp.summary === 'left' || cmp.summary === 'both') {
titleText = t("Original Has Modifications"); titleText = t("Original Has Modifications");
buttonText = t("Overwrite"); buttonText = t("Overwrite");
warningText = `${warningText} ${t("Be careful, the original has changes " + warningText = `${warningText} ${t("Be careful, the original has changes \
"not in this document. Those changes will be overwritten.")}`; not in this document. Those changes will be overwritten.")}`;
} else if (cmp.summary === 'unrelated') { } else if (cmp.summary === 'unrelated') {
titleText = t("Original Looks Unrelated"); titleText = t("Original Looks Unrelated");
buttonText = t("Overwrite"); buttonText = t("Overwrite");

View File

@ -102,7 +102,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
return cssHoverCircle({ style: `margin: 5px;` }, return cssHoverCircle({ style: `margin: 5px;` },
cssTopBarBtn('Share', dom.cls('tour-share-icon')), cssTopBarBtn('Share', dom.cls('tour-share-icon')),
menu(menuCreateFunc, {placement: 'bottom-end'}), menu(menuCreateFunc, {placement: 'bottom-end'}),
hoverTooltip('Share', {key: 'topBarBtnTooltip'}), hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
testId('tb-share'), testId('tb-share'),
); );
} else if (options.buttonAction) { } else if (options.buttonAction) {
@ -115,7 +115,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
cssShareCircle( cssShareCircle(
cssShareIcon('Share'), cssShareIcon('Share'),
menu(menuCreateFunc, {placement: 'bottom-end'}), menu(menuCreateFunc, {placement: 'bottom-end'}),
hoverTooltip('Share', {key: 'topBarBtnTooltip'}), hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
testId('tb-share'), testId('tb-share'),
), ),
); );
@ -128,7 +128,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
cssShareIcon('Share') cssShareIcon('Share')
), ),
menu(menuCreateFunc, {placement: 'bottom-end'}), menu(menuCreateFunc, {placement: 'bottom-end'}),
hoverTooltip('Share', {key: 'topBarBtnTooltip'}), hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
testId('tb-share'), testId('tb-share'),
); );
} }

View File

@ -5,6 +5,7 @@
* *
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions. * It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
*/ */
import { makeT } from 'app/client/lib/localization';
import {commonUrls} from 'app/common/gristUrls'; import {commonUrls} from 'app/common/gristUrls';
import {capitalizeFirstWord, isLongerThan} from 'app/common/gutil'; import {capitalizeFirstWord, isLongerThan} from 'app/common/gutil';
import {FullUser} from 'app/common/LoginSessionAPI'; import {FullUser} from 'app/common/LoginSessionAPI';
@ -42,6 +43,8 @@ import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
import {confirmModal, cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle, import {confirmModal, cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
IModalControl, modal} from 'app/client/ui2018/modals'; IModalControl, modal} from 'app/client/ui2018/modals';
const t = makeT('UserManager');
export interface IUserManagerOptions { export interface IUserManagerOptions {
permissionData: Promise<PermissionData>; permissionData: Promise<PermissionData>;
activeUser: FullUser|null; activeUser: FullUser|null;
@ -101,15 +104,15 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti
} }
}; };
if (model.isSelfRemoved.get()) { if (model.isSelfRemoved.get()) {
const name = resourceName(model.resourceType); const resourceType = resourceName(model.resourceType);
confirmModal( confirmModal(
`You are about to remove your own access to this ${name}`, t(`You are about to remove your own access to this {{resourceType}}`, { resourceType }),
'Remove my access', tryToSaveChanges, t('Remove my access'), tryToSaveChanges,
{ {
explanation: ( explanation: (
'Once you have removed your own access, ' + t(`Once you have removed your own access, \
'you will not be able to get it back without assistance ' + you will not be able to get it back without assistance \
`from someone else with sufficient access to the ${name}.` from someone else with sufficient access to the {{resourceType}}.`, { resourceType })
), ),
} }
); );
@ -162,22 +165,22 @@ function buildUserManagerModal(
cssModalButtons( cssModalButtons(
{ style: 'margin: 32px 64px; display: flex;' }, { style: 'margin: 32px 64px; display: flex;' },
(model.isPublicMember ? null : (model.isPublicMember ? null :
bigPrimaryButton('Confirm', bigPrimaryButton(t('Confirm'),
dom.boolAttr('disabled', (use) => !use(model.isAnythingChanged)), dom.boolAttr('disabled', (use) => !use(model.isAnythingChanged)),
dom.on('click', () => onConfirm(ctl)), dom.on('click', () => onConfirm(ctl)),
testId('um-confirm') testId('um-confirm')
) )
), ),
bigBasicButton( bigBasicButton(
model.isPublicMember ? 'Close' : 'Cancel', model.isPublicMember ? t('Close') : t('Cancel'),
dom.on('click', () => ctl.close()), dom.on('click', () => ctl.close()),
testId('um-cancel') testId('um-cancel')
), ),
(model.resourceType === 'document' && model.gristDoc && !model.isPersonal (model.resourceType === 'document' && model.gristDoc && !model.isPersonal
? withInfoTooltip( ? withInfoTooltip(
cssLink({href: urlState().makeUrl({docPage: 'acl'})}, cssLink({href: urlState().makeUrl({docPage: 'acl'})},
dom.text(use => use(model.isAnythingChanged) ? 'Save & ' : ''), dom.text(use => use(model.isAnythingChanged) ? t('Save & ') : ''),
'Open Access Rules', t('Open Access Rules'),
dom.on('click', (ev) => { dom.on('click', (ev) => {
ev.preventDefault(); ev.preventDefault();
return onConfirm(ctl).then(() => urlState().pushUrl({docPage: 'acl'})); return onConfirm(ctl).then(() => urlState().pushUrl({docPage: 'acl'}));
@ -268,7 +271,7 @@ export class UserManager extends Disposable {
return dom('div', return dom('div',
cssOptionRowMultiple( cssOptionRowMultiple(
icon('AddUser'), icon('AddUser'),
cssLabel('Invite multiple'), cssLabel(t('Invite multiple')),
dom.on('click', (_ev) => buildMultiUserManagerModal( dom.on('click', (_ev) => buildMultiUserManagerModal(
this, this,
this._model, this._model,
@ -286,30 +289,31 @@ export class UserManager extends Disposable {
), ),
publicMember ? dom('span', { style: `float: right;` }, publicMember ? dom('span', { style: `float: right;` },
cssSmallPublicMemberIcon('PublicFilled'), cssSmallPublicMemberIcon('PublicFilled'),
dom('span', 'Public access: '), dom('span', t('Public access: ')),
cssOptionBtn( cssOptionBtn(
menu(() => { menu(() => {
tooltipControl?.close(); tooltipControl?.close();
return [ return [
menuItem(() => publicMember.access.set(roles.VIEWER), 'On', testId(`um-public-option`)), menuItem(() => publicMember.access.set(roles.VIEWER), t('On'), testId(`um-public-option`)),
menuItem(() => publicMember.access.set(null), 'Off', menuItem(() => publicMember.access.set(null), t('Off'),
// Disable null access if anonymous access is inherited. // Disable null access if anonymous access is inherited.
dom.cls('disabled', (use) => use(publicMember.inheritedAccess) !== null), dom.cls('disabled', (use) => use(publicMember.inheritedAccess) !== null),
testId(`um-public-option`) testId(`um-public-option`)
), ),
// If the 'Off' setting is disabled, show an explanation. // If the 'Off' setting is disabled, show an explanation.
dom.maybe((use) => use(publicMember.inheritedAccess) !== null, () => menuText( dom.maybe((use) => use(publicMember.inheritedAccess) !== null, () => menuText(
`Public access inherited from ${getResourceParent(this._model.resourceType)}. ` + t(`Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.`,
`To remove, set 'Inherit access' option to 'None'.`)) { parent: getResourceParent(this._model.resourceType) }
)))
]; ];
}), }),
dom.text((use) => use(publicMember.effectiveAccess) ? 'On' : 'Off'), dom.text((use) => use(publicMember.effectiveAccess) ? t('On') : t('Off')),
cssCollapseIcon('Collapse'), cssCollapseIcon('Collapse'),
testId('um-public-access') testId('um-public-access')
), ),
hoverTooltip((ctl) => { hoverTooltip((ctl) => {
tooltipControl = ctl; tooltipControl = ctl;
return 'Allow anyone with the link to open.'; return t('Allow anyone with the link to open.');
}), }),
) : null, ) : null,
), ),
@ -373,19 +377,23 @@ export class UserManager extends Disposable {
const annotation = annotations.users.get(member.email); const annotation = annotations.users.get(member.email);
if (!annotation) { return null; } if (!annotation) { return null; }
if (annotation.isSupport) { if (annotation.isSupport) {
return cssMemberType('Grist support'); return cssMemberType(t('Grist support'));
} }
if (annotation.isMember && annotations.hasTeam) { if (annotation.isMember && annotations.hasTeam) {
return cssMemberType('Team member'); return cssMemberType(t('Team member'));
} }
const collaborator = annotations.hasTeam ? 'guest' : 'free collaborator'; const collaborator = annotations.hasTeam ? t('guest') : t('free collaborator');
const limit = annotation.collaboratorLimit; const limit = annotation.collaboratorLimit;
if (!limit || !limit.top) { return null; } if (!limit || !limit.top) { return null; }
const elements: HTMLSpanElement[] = []; const elements: HTMLSpanElement[] = [];
if (limit.at <= limit.top) { if (limit.at <= limit.top) {
elements.push(cssMemberType(`${limit.at} of ${limit.top} ${collaborator}s`)); elements.push(cssMemberType(
t(`{{limitAt}} of {{limitTop}} {{collaborator}}s`, { limitAt: limit.at, limitTop: limit.top, collaborator }))
);
} else { } else {
elements.push(cssMemberTypeProblem(`${capitalizeFirstWord(collaborator)} limit exceeded`)); elements.push(cssMemberTypeProblem(
t(`{{collaborator}} limit exceeded`, { collaborator: capitalizeFirstWord(collaborator) }))
);
} }
if (annotations.hasTeam) { if (annotations.hasTeam) {
// Add a link for adding a member. For a doc, streamline this so user can make // Add a link for adding a member. For a doc, streamline this so user can make
@ -401,10 +409,10 @@ export class UserManager extends Disposable {
{ email: member.email }).catch(reportError); { email: member.email }).catch(reportError);
} }
}), }),
`Add ${member.name || 'member'} to your team`)); t(`Add {{member}} to your team`, { member: member.name || t('member') })));
} else if (limit.at >= limit.top) { } else if (limit.at >= limit.top) {
elements.push(cssLink({href: commonUrls.plans, target: '_blank'}, elements.push(cssLink({href: commonUrls.plans, target: '_blank'},
'Create a team to share with more people')); t('Create a team to share with more people')));
} }
return elements; return elements;
}); });
@ -418,13 +426,13 @@ export class UserManager extends Disposable {
let memberType: string; let memberType: string;
if (annotation.isSupport) { if (annotation.isSupport) {
memberType = 'Grist support'; memberType = t('Grist support');
} else if (annotation.isMember && annotations.hasTeam) { } else if (annotation.isMember && annotations.hasTeam) {
memberType = 'Team member'; memberType = t('Team member');
} else if (annotations.hasTeam) { } else if (annotations.hasTeam) {
memberType = 'Outside collaborator'; memberType = t('Outside collaborator');
} else { } else {
memberType = 'Collaborator'; memberType = t('Collaborator');
} }
return cssMemberType(memberType, testId('um-member-annotation')); return cssMemberType(memberType, testId('um-member-annotation'));
@ -439,8 +447,8 @@ export class UserManager extends Disposable {
cssMemberListItem( cssMemberListItem(
cssPublicMemberIcon('PublicFilled'), cssPublicMemberIcon('PublicFilled'),
cssMemberText( cssMemberText(
cssMemberPrimary('Public Access'), cssMemberPrimary(t('Public Access')),
cssMemberSecondary('Anyone with link ', makeCopyBtn(this._options.linkToCopy)), cssMemberSecondary(t('Anyone with link '), makeCopyBtn(this._options.linkToCopy)),
), ),
this._memberRoleSelector(publicMember.effectiveAccess, publicMember.inheritedAccess, false, this._memberRoleSelector(publicMember.effectiveAccess, publicMember.inheritedAccess, false,
this._model.publicUserSelectOptions this._model.publicUserSelectOptions
@ -472,12 +480,12 @@ export class UserManager extends Disposable {
cssMemberPrimary(name, testId('um-member-name')), cssMemberPrimary(name, testId('um-member-name')),
activeUser?.email ? cssMemberSecondary(activeUser.email) : null, activeUser?.email ? cssMemberSecondary(activeUser.email) : null,
cssMemberPublicAccess( cssMemberPublicAccess(
dom('span', 'Public access', testId('um-member-annotation')), dom('span', t('Public access'), testId('um-member-annotation')),
cssPublicAccessIcon('PublicFilled'), cssPublicAccessIcon('PublicFilled'),
), ),
), ),
cssRoleBtn( cssRoleBtn(
accessLabel ?? 'Guest', accessLabel ?? t('Guest'),
cssCollapseIcon('Collapse'), cssCollapseIcon('Collapse'),
dom.cls('disabled'), dom.cls('disabled'),
testId('um-member-role'), testId('um-member-role'),
@ -522,23 +530,24 @@ export class UserManager extends Disposable {
) )
), ),
// If the user's access is inherited, give an explanation on how to change it. // If the user's access is inherited, give an explanation on how to change it.
isActiveUser ? menuText(`User may not modify their own access.`) : null, isActiveUser ? menuText(t(`User may not modify their own access.`)) : null,
// If the user's access is inherited, give an explanation on how to change it. // If the user's access is inherited, give an explanation on how to change it.
dom.maybe((use) => use(inherited) && !isActiveUser, () => menuText( dom.maybe((use) => use(inherited) && !isActiveUser, () => menuText(
`User inherits permissions from ${getResourceParent(this._model.resourceType)}. To remove, ` + t(`User inherits permissions from {{parent}}. To remove, \
`set 'Inherit access' option to 'None'.`)), set 'Inherit access' option to 'None'.`, { parent: getResourceParent(this._model.resourceType) }))),
// If the user is a guest, give a description of the guest permission. // If the user is a guest, give a description of the guest permission.
dom.maybe((use) => !this._model.isOrg && use(role) === roles.GUEST, () => menuText( dom.maybe((use) => !this._model.isOrg && use(role) === roles.GUEST, () => menuText(
`User has view access to ${this._model.resourceType} resulting from manually-set access ` + t(`User has view access to {{resource}} resulting from manually-set access \
`to resources inside. If removed here, this user will lose access to resources inside.`)), to resources inside. If removed here, this user will lose access to resources inside.`,
this._model.isOrg ? menuText(`No default access allows access to be ` + { resource: this._model.resourceType }))),
`granted to individual documents or workspaces, rather than the full team site.`) : null this._model.isOrg ? menuText(t(`No default access allows access to be \
granted to individual documents or workspaces, rather than the full team site.`)) : null
]), ]),
dom.text((use) => { dom.text((use) => {
// Get the label of the active role. Note that the 'Guest' role is assigned when the role // Get the label of the active role. Note that the 'Guest' role is assigned when the role
// is not found because it is not included as a selection. // is not found because it is not included as a selection.
const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(role) === _role.value); const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(role) === _role.value);
return activeRole ? activeRole.label : "Guest"; return activeRole ? activeRole.label : t("Guest");
}), }),
cssCollapseIcon('Collapse'), cssCollapseIcon('Collapse'),
this._model.isPersonal ? dom.cls('disabled') : null, this._model.isPersonal ? dom.cls('disabled') : null,
@ -634,7 +643,7 @@ function getFullUser(member: IEditableMember): FullUser {
// Create a "Copy Link" button. // Create a "Copy Link" button.
function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[]) { function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[]) {
return linkToCopy && cssCopyBtn(cssCopyIcon('Copy'), 'Copy Link', return linkToCopy && cssCopyBtn(cssCopyIcon('Copy'), t('Copy Link'),
dom.on('click', (ev, elem) => copyLink(elem, linkToCopy)), dom.on('click', (ev, elem) => copyLink(elem, linkToCopy)),
testId('um-copy-link'), testId('um-copy-link'),
...domArgs, ...domArgs,
@ -646,7 +655,7 @@ function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[])
async function copyLink(elem: HTMLElement, link: string) { async function copyLink(elem: HTMLElement, link: string) {
await copyToClipboard(link); await copyToClipboard(link);
setTestState({clipboard: link}); setTestState({clipboard: link});
showTransientTooltip(elem, 'Link copied to clipboard', {key: 'copy-doc-link'}); showTransientTooltip(elem, t('Link copied to clipboard'), { key: 'copy-doc-link' });
} }
async function manageTeam(appModel: AppModel, async function manageTeam(appModel: AppModel,
@ -808,9 +817,9 @@ const cssMemberPublicAccess = styled(cssMemberSecondary, `
function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) { function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) {
switch (resourceType) { switch (resourceType) {
case 'organization': { case 'organization': {
if (personal) { return 'Your role for this team site'; } if (personal) { return t('Your role for this team site'); }
return [ return [
'Manage members of team site', t('Manage members of team site'),
!resource ? null : cssOrgName( !resource ? null : cssOrgName(
`${(resource as Organization).name} (`, `${(resource as Organization).name} (`,
cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`), cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`),
@ -819,12 +828,14 @@ function renderTitle(resourceType: ResourceType, resource?: Resource, personal?:
]; ];
} }
default: { default: {
return personal ? `Your role for this ${resourceType}` : `Invite people to ${resourceType}`; return personal ?
t(`Your role for this {{resourceType}}`, { resourceType }) :
t(`Invite people to {{resourceType}}`, { resourceType });
} }
} }
} }
// Rename organization to team site. // Rename organization to team site.
function resourceName(resourceType: ResourceType): string { function resourceName(resourceType: ResourceType): string {
return resourceType === 'organization' ? 'team site' : resourceType; return resourceType === 'organization' ? t('team site') : resourceType;
} }

View File

@ -10,7 +10,7 @@ import { dom, styled } from "grainjs";
const t = makeT('WelcomeTour'); const t = makeT('WelcomeTour');
export const welcomeTour: IOnBoardingMsg[] = [ export const WelcomeTour: IOnBoardingMsg[] = [
{ {
title: t('Editing Data'), title: t('Editing Data'),
body: () => [ body: () => [
@ -97,7 +97,7 @@ export const welcomeTour: IOnBoardingMsg[] = [
export function startWelcomeTour(onFinishCB: () => void) { export function startWelcomeTour(onFinishCB: () => void) {
commands.allCommands.fieldTabOpen.run(); commands.allCommands.fieldTabOpen.run();
startOnBoarding(welcomeTour, onFinishCB); startOnBoarding(WelcomeTour, onFinishCB);
} }
const TopBarButtonIcon = styled(icon, ` const TopBarButtonIcon = styled(icon, `

View File

@ -35,8 +35,8 @@ export function createForbiddenPage(appModel: AppModel, message?: string) {
return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [ return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [
dom.domComputed(appModel.currentValidUser, user => user ? [ dom.domComputed(appModel.currentValidUser, user => user ? [
cssErrorText(message || t("You do not have access to this organization's documents.")), cssErrorText(message || t("You do not have access to this organization's documents.")),
cssErrorText(t("You are signed in as {{email}}. You can sign in with a different " + cssErrorText(t("You are signed in as {{email}}. You can sign in with a different \
"account, or ask an administrator for access.", {email: dom('b', user.email)})), account, or ask an administrator for access.", {email: dom('b', user.email)})),
] : [ ] : [
// This page is not normally shown because a logged out user with no access will get // This page is not normally shown because a logged out user with no access will get
// redirected to log in. But it may be seen if a user logs out and returns to a cached // redirected to log in. But it may be seen if a user logs out and returns to a cached

View File

@ -10,6 +10,9 @@ import { icon } from "app/client/ui2018/icons";
import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel"; import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel";
import { mergeWith } from "lodash"; import { mergeWith } from "lodash";
import { getOptionFull, SimpleList } from "../lib/simpleList"; import { getOptionFull, SimpleList } from "../lib/simpleList";
import { makeT } from 'app/client/lib/localization';
const t = makeT('searchDropdown');
const testId = makeTestId('test-sd-'); const testId = makeTestId('test-sd-');
@ -92,7 +95,7 @@ class DropdownWithSearch<T> extends Disposable {
cssMenuHeader( cssMenuHeader(
cssSearchIcon('Search'), cssSearchIcon('Search'),
this._inputElem = cssSearch( this._inputElem = cssSearch(
{placeholder: this._options.placeholder || 'Search'}, {placeholder: this._options.placeholder || t('Search')},
dom.on('input', () => { this._update(); }), dom.on('input', () => { this._update(); }),
dom.on('blur', () => setTimeout(() => this._inputElem.focus(), 0)), dom.on('blur', () => setTimeout(() => this._inputElem.focus(), 0)),
), ),

View File

@ -177,7 +177,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
cssTopBarBtn('Search', cssTopBarBtn('Search',
testId('icon'), testId('icon'),
dom.on('click', focusAndSelect), dom.on('click', focusAndSelect),
hoverTooltip('Search', {key: 'topBarBtnTooltip'}), hoverTooltip(t('Search'), {key: 'topBarBtnTooltip'}),
) )
), ),
expandedSearch( expandedSearch(

View File

@ -39,7 +39,9 @@
"View As": "View As", "View As": "View As",
"Seed rules": "Seed rules", "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" "Permission to edit document structure": "Permission to edit document structure",
"This default should be changed if editors' access is to be limited. ": "This default should be changed if editors' access is to be limited. ",
"Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions."
}, },
"AccountPage": { "AccountPage": {
"API": "API", "API": "API",
@ -589,7 +591,8 @@
"Send to Google Drive": "Send to Google Drive", "Send to Google Drive": "Send to Google Drive",
"Show in folder": "Show in folder", "Show in folder": "Show in folder",
"Unsaved": "Unsaved", "Unsaved": "Unsaved",
"Work on a Copy": "Work on a Copy" "Work on a Copy": "Work on a Copy",
"Share": "Share"
}, },
"SiteSwitcher": { "SiteSwitcher": {
"Create new team site": "Create new team site", "Create new team site": "Create new team site",
@ -801,7 +804,8 @@
"Find Next ": "Find Next ", "Find Next ": "Find Next ",
"Find Previous ": "Find Previous ", "Find Previous ": "Find Previous ",
"No results": "No results", "No results": "No results",
"Search in document": "Search in document" "Search in document": "Search in document",
"Search": "Search"
}, },
"sendToDrive": { "sendToDrive": {
"Sending file to Google Drive": "Sending file to Google Drive" "Sending file to Google Drive": "Sending file to Google Drive"
@ -979,7 +983,9 @@
"Add New": "Add New", "Add New": "Add New",
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.", "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.",
"Anchor Links": "Anchor Links", "Anchor Links": "Anchor Links",
"Custom Widgets": "Custom Widgets" "Custom Widgets": "Custom Widgets",
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.",
"You can choose one of our pre-made widgets or embed your own by providing its full URL.": "You can choose one of our pre-made widgets or embed your own by providing its full URL."
}, },
"DescriptionConfig": { "DescriptionConfig": {
"DESCRIPTION": "DESCRIPTION" "DESCRIPTION": "DESCRIPTION"
@ -1041,6 +1047,61 @@
"You can always switch sites using the account menu.": "You can always switch sites using the account menu.", "You can always switch sites using the account menu.": "You can always switch sites using the account menu.",
"You have access to the following Grist sites.": "You have access to the following Grist sites." "You have access to the following Grist sites.": "You have access to the following Grist sites."
}, },
"DescriptionTextArea": {
"DESCRIPTION": "DESCRIPTION"
},
"UserManager": {
"Add {{member}} to your team": "Add {{member}} to your team",
"Allow anyone with the link to open.": "Allow anyone with the link to open.",
"Anyone with link ": "Anyone with link ",
"Cancel": "Cancel",
"Close": "Close",
"Collaborator": "Collaborator",
"Confirm": "Confirm",
"Copy Link": "Copy Link",
"Create a team to share with more people": "Create a team to share with more people",
"Grist support": "Grist support",
"Guest": "Guest",
"Invite multiple": "Invite multiple",
"Invite people to {{resourceType}}": "Invite people to {{resourceType}}",
"Link copied to clipboard": "Link copied to clipboard",
"Manage members of team site": "Manage members of team site",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.",
"Off": "Off",
"On": "On",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.",
"Open Access Rules": "Open Access Rules",
"Outside collaborator": "Outside collaborator",
"Public Access": "Public Access",
"Public access": "Public access",
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.",
"Public access: ": "Public access: ",
"Remove my access": "Remove my access",
"Save & ": "Save & ",
"Team member": "Team member",
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.",
"User may not modify their own access.": "User may not modify their own access.",
"Your role for this team site": "Your role for this team site",
"Your role for this {{resourceType}}": "Your role for this {{resourceType}}",
"free collaborator": "free collaborator",
"guest": "guest",
"member": "member",
"team site": "team site",
"{{collaborator}} limit exceeded": "{{collaborator}} limit exceeded",
"{{limitAt}} of {{limitTop}} {{collaborator}}s": "{{limitAt}} of {{limitTop}} {{collaborator}}s",
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.",
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.",
"User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.",
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.",
"You are about to remove your own access to this {{resourceType}}": "You are about to remove your own access to this {{resourceType}}"
},
"SearchModel": {
"Search all pages": "Search all pages",
"Search all tables": "Search all tables"
},
"searchDropdown": {
"Search": "Search"
},
"SupportGristNudge": { "SupportGristNudge": {
"Close": "Close", "Close": "Close",
"Contribute": "Contribute", "Contribute": "Contribute",