diff --git a/app/client/aclui/AccessRules.ts b/app/client/aclui/AccessRules.ts index d2912703..e205bce1 100644 --- a/app/client/aclui/AccessRules.ts +++ b/app/client/aclui/AccessRules.ts @@ -1178,8 +1178,8 @@ Useful for examples and templates, but not for sensitive data.`), }, SchemaEdit: { name: t("Permission to edit document structure"), - 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."), + 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."), availableBits: ['schemaEdit'], ...schemaEditRules.denyEditors, }, @@ -1323,7 +1323,7 @@ class SpecialSchemaObsRuleSet extends SpecialObsRuleSet { return dom.maybe( (use) => use(this._body).every(rule => rule.isBuiltInOrEmpty(use)), () => 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'}, 'Dismiss', dom.on('click', () => this._allowEditors('confirm'))), testId('rule-schema-edit-warning'), diff --git a/app/client/components/ChartView.ts b/app/client/components/ChartView.ts index 8fd99763..7b33169b 100644 --- a/app/client/components/ChartView.ts +++ b/app/client/components/ChartView.ts @@ -660,8 +660,9 @@ export class ChartConfig extends GrainJSDisposable { ), 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 === 'separate' ? cssRowHelp(t("Each Y series is followed by two series, for " + - "top and bottom error bars.")) + value === 'separate' ? cssRowHelp( + t("Each Y series is followed by two series, for top and bottom error bars.") + ) : null ), ]), diff --git a/app/client/models/DocPageModel.ts b/app/client/models/DocPageModel.ts index 84aeb208..3c711e2d 100644 --- a/app/client/models/DocPageModel.ts +++ b/app/client/models/DocPageModel.ts @@ -265,10 +265,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel { { explanation: ( isDocOwner - ? t("You can try reloading the document, or using recovery mode. " + - "Recovery mode opens the document to be fully accessible to " + - "owners, and inaccessible to others. It also disables " + - "formulas. [{{error}}]", {error: err.message}) + ? t("You can try reloading the document, or using recovery mode. \ +Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. \ +It also disables formulas. [{{error}}]", {error: err.message}) : isDenied ? 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}) diff --git a/app/client/models/SearchModel.ts b/app/client/models/SearchModel.ts index f3525c67..2a791e04 100644 --- a/app/client/models/SearchModel.ts +++ b/app/client/models/SearchModel.ts @@ -12,6 +12,9 @@ import {TableData} from 'app/common/TableData'; import {BaseFormatter} from 'app/common/ValueFormatter'; import {Computed, Disposable, Observable} from 'grainjs'; 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. @@ -201,7 +204,7 @@ class FinderImpl implements IFinder { // sort in order that is the same as on the raw data list page, .sort((a, b) => nativeCompare(a.tableNameDef.peek(), b.tableNameDef.peek())) // get rawViewSection, - .map(t => t.rawViewSection.peek()) + .map(table => table.rawViewSection.peek()) // and test if it isn't an empty record. .filter(s => Boolean(s.id.peek())); // Pretend that those are pages. @@ -218,7 +221,7 @@ class FinderImpl implements IFinder { // Else read all visible pages. const pages = this._gristDoc.docModel.visibleDocPages.peek(); 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; } } @@ -468,7 +471,7 @@ export class SearchModelImpl extends Disposable implements SearchModel { this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } })); 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 // previous page that is not shown anymore). Also revert noMatch flag when in single page mode. diff --git a/app/client/ui/AccountPage.ts b/app/client/ui/AccountPage.ts index 88d95ad8..452c6284 100644 --- a/app/client/ui/AccountPage.ts +++ b/app/client/ui/AccountPage.ts @@ -131,9 +131,8 @@ export class AccountPage extends Disposable { ), css.subHeader(t("Two-factor authentication")), css.description( - 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, " + - "even if someone knows your password.") + 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, even if someone knows your password.") ), dom.create(MFAConfig, user), ), diff --git a/app/client/ui/ApiKey.ts b/app/client/ui/ApiKey.ts index 3d6f313d..aa269bbc 100644 --- a/app/client/ui/ApiKey.ts +++ b/app/client/ui/ApiKey.ts @@ -78,8 +78,8 @@ export class ApiKey extends Disposable { dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [ basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'), dom.boolAttr('disabled', this._loading)), - description(t("By generating an API key, you will be able to " + - "make API calls for your own account."), testId('description')), + description(t("By generating an API key, you will be able to \ +make API calls for your own account."), testId('description')), ]), ); } @@ -117,8 +117,8 @@ export class ApiKey extends Disposable { () => this._onDelete(), { explanation: t( - "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?" + "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?" ), } ); diff --git a/app/client/ui/DocTour.ts b/app/client/ui/DocTour.ts index 86b6bb10..d1c2385f 100644 --- a/app/client/ui/DocTour.ts +++ b/app/client/ui/DocTour.ts @@ -21,8 +21,8 @@ export async function startDocTour(docData: DocData, docComm: DocComm, onFinishC const invalidDocTour: IOnBoardingMsg[] = [{ title: t("No valid document tour"), - 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."), + 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."), selector: 'document', showHasModal: true, }]; diff --git a/app/client/ui/ExampleInfo.ts b/app/client/ui/ExampleInfo.ts index 471b2e34..1781f628 100644 --- a/app/client/ui/ExampleInfo.ts +++ b/app/client/ui/ExampleInfo.ts @@ -36,8 +36,8 @@ export const buildExamples = (): IExampleInfo[] => [{ tutorialUrl: 'https://support.getgrist.com/investment-research/', welcomeCard: { title: t("Welcome to the Investment Research template"), - text: t("Check out our related tutorial to learn how to create " + - "summary tables and charts, and to link charts dynamically."), + text: t("Check out our related tutorial to learn how to create \ +summary tables and charts, and to link charts dynamically."), tutorialName: t("Tutorial: Analyze & Visualize"), }, }, { diff --git a/app/client/ui/GristTooltips.ts b/app/client/ui/GristTooltips.ts index 04264845..6c997f95 100644 --- a/app/client/ui/GristTooltips.ts +++ b/app/client/ui/GristTooltips.ts @@ -50,8 +50,7 @@ export const GristTooltips: Record = { t('Formulas that trigger in certain cases, and store the calculated value as data.') ), dom('div', - t('Useful for storing the timestamp or author of a new record, data cleaning, and ' - + 'more.') + t('Useful for storing the timestamp or author of a new record, data cleaning, and more.') ), dom('div', cssLink({href: commonUrls.helpTriggerFormulas, target: '_blank'}, t('Learn more.')), @@ -76,8 +75,8 @@ export const GristTooltips: Record = { ), openAccessRules: (...args: DomElementArg[]) => cssTooltipContent( 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.') + t('Access rules give you the power to create nuanced rules to determine who can \ +see or edit which parts of your document.') ), dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.')), @@ -126,8 +125,8 @@ export const GristBehavioralPrompts: Record t('Reference Columns'), content: (...args: DomElementArg[]) => cssTooltipContent( dom('div', t('Select the table to link to.')), - 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.', { + 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.', { entire: cssItalicizedText(t('entire')) })), dom('div', @@ -140,8 +139,8 @@ export const GristBehavioralPrompts: Record t('Raw Data page'), content: (...args: DomElementArg[]) => cssTooltipContent( - dom('div', t('The Raw Data page lists all data tables in your document, ' - + 'including summary tables and tables not included in page layouts.')), + dom('div', t('The Raw Data page lists all data tables in your document, \ +including summary tables and tables not included in page layouts.')), dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))), ...args, ), @@ -150,8 +149,8 @@ export const GristBehavioralPrompts: Record t('Access Rules'), content: (...args: DomElementArg[]) => cssTooltipContent( - 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.')), + 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.')), dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))), ...args, ), @@ -209,8 +208,7 @@ export const GristBehavioralPrompts: Record t('Add New'), content: (...args: DomElementArg[]) => cssTooltipContent( - dom('div', t('Click the Add New button to create new documents or workspaces, ' - + 'or import data.')), + dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')), ...args, ), deploymentTypes: ['saas'], @@ -219,8 +217,7 @@ export const GristBehavioralPrompts: Record t('Anchor Links'), content: (...args: DomElementArg[]) => cssTooltipContent( dom('div', - t('To make an anchor link that takes the user to a specific cell, click on' - + ' a row and press {{shortcut}}.', + t('To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.', { shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])), } @@ -235,8 +232,7 @@ export const GristBehavioralPrompts: Record cssTooltipContent( dom('div', t( - '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.' ), ), dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))), diff --git a/app/client/ui/MakeCopyMenu.ts b/app/client/ui/MakeCopyMenu.ts index 229fcedc..6c757b00 100644 --- a/app/client/ui/MakeCopyMenu.ts +++ b/app/client/ui/MakeCopyMenu.ts @@ -41,8 +41,8 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a if (cmp.summary === 'left' || cmp.summary === 'both') { titleText = t("Original Has Modifications"); buttonText = t("Overwrite"); - warningText = `${warningText} ${t("Be careful, the original has changes " + - "not in this document. Those changes will be overwritten.")}`; + warningText = `${warningText} ${t("Be careful, the original has changes \ +not in this document. Those changes will be overwritten.")}`; } else if (cmp.summary === 'unrelated') { titleText = t("Original Looks Unrelated"); buttonText = t("Overwrite"); diff --git a/app/client/ui/ShareMenu.ts b/app/client/ui/ShareMenu.ts index a9b988ef..fe3fa365 100644 --- a/app/client/ui/ShareMenu.ts +++ b/app/client/ui/ShareMenu.ts @@ -102,7 +102,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc, return cssHoverCircle({ style: `margin: 5px;` }, cssTopBarBtn('Share', dom.cls('tour-share-icon')), menu(menuCreateFunc, {placement: 'bottom-end'}), - hoverTooltip('Share', {key: 'topBarBtnTooltip'}), + hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}), testId('tb-share'), ); } else if (options.buttonAction) { @@ -115,7 +115,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc, cssShareCircle( cssShareIcon('Share'), menu(menuCreateFunc, {placement: 'bottom-end'}), - hoverTooltip('Share', {key: 'topBarBtnTooltip'}), + hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}), testId('tb-share'), ), ); @@ -128,7 +128,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc, cssShareIcon('Share') ), menu(menuCreateFunc, {placement: 'bottom-end'}), - hoverTooltip('Share', {key: 'topBarBtnTooltip'}), + hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}), testId('tb-share'), ); } diff --git a/app/client/ui/UserManager.ts b/app/client/ui/UserManager.ts index f4f55a54..2f2f55a4 100644 --- a/app/client/ui/UserManager.ts +++ b/app/client/ui/UserManager.ts @@ -5,6 +5,7 @@ * * 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 {capitalizeFirstWord, isLongerThan} from 'app/common/gutil'; 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, IModalControl, modal} from 'app/client/ui2018/modals'; +const t = makeT('UserManager'); + export interface IUserManagerOptions { permissionData: Promise; activeUser: FullUser|null; @@ -101,15 +104,15 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti } }; if (model.isSelfRemoved.get()) { - const name = resourceName(model.resourceType); + const resourceType = resourceName(model.resourceType); confirmModal( - `You are about to remove your own access to this ${name}`, - 'Remove my access', tryToSaveChanges, + t(`You are about to remove your own access to this {{resourceType}}`, { resourceType }), + t('Remove my access'), tryToSaveChanges, { explanation: ( - '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}.` + t(`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}}.`, { resourceType }) ), } ); @@ -162,22 +165,22 @@ function buildUserManagerModal( cssModalButtons( { style: 'margin: 32px 64px; display: flex;' }, (model.isPublicMember ? null : - bigPrimaryButton('Confirm', + bigPrimaryButton(t('Confirm'), dom.boolAttr('disabled', (use) => !use(model.isAnythingChanged)), dom.on('click', () => onConfirm(ctl)), testId('um-confirm') ) ), bigBasicButton( - model.isPublicMember ? 'Close' : 'Cancel', + model.isPublicMember ? t('Close') : t('Cancel'), dom.on('click', () => ctl.close()), testId('um-cancel') ), (model.resourceType === 'document' && model.gristDoc && !model.isPersonal ? withInfoTooltip( cssLink({href: urlState().makeUrl({docPage: 'acl'})}, - dom.text(use => use(model.isAnythingChanged) ? 'Save & ' : ''), - 'Open Access Rules', + dom.text(use => use(model.isAnythingChanged) ? t('Save & ') : ''), + t('Open Access Rules'), dom.on('click', (ev) => { ev.preventDefault(); return onConfirm(ctl).then(() => urlState().pushUrl({docPage: 'acl'})); @@ -268,7 +271,7 @@ export class UserManager extends Disposable { return dom('div', cssOptionRowMultiple( icon('AddUser'), - cssLabel('Invite multiple'), + cssLabel(t('Invite multiple')), dom.on('click', (_ev) => buildMultiUserManagerModal( this, this._model, @@ -286,30 +289,31 @@ export class UserManager extends Disposable { ), publicMember ? dom('span', { style: `float: right;` }, cssSmallPublicMemberIcon('PublicFilled'), - dom('span', 'Public access: '), + dom('span', t('Public access: ')), cssOptionBtn( menu(() => { tooltipControl?.close(); return [ - menuItem(() => publicMember.access.set(roles.VIEWER), 'On', testId(`um-public-option`)), - menuItem(() => publicMember.access.set(null), 'Off', + menuItem(() => publicMember.access.set(roles.VIEWER), t('On'), testId(`um-public-option`)), + menuItem(() => publicMember.access.set(null), t('Off'), // Disable null access if anonymous access is inherited. dom.cls('disabled', (use) => use(publicMember.inheritedAccess) !== null), testId(`um-public-option`) ), // If the 'Off' setting is disabled, show an explanation. dom.maybe((use) => use(publicMember.inheritedAccess) !== null, () => menuText( - `Public access inherited from ${getResourceParent(this._model.resourceType)}. ` + - `To remove, set 'Inherit access' option to 'None'.`)) + t(`Public access inherited from {{parent}}. 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'), testId('um-public-access') ), hoverTooltip((ctl) => { tooltipControl = ctl; - return 'Allow anyone with the link to open.'; + return t('Allow anyone with the link to open.'); }), ) : null, ), @@ -373,19 +377,23 @@ export class UserManager extends Disposable { const annotation = annotations.users.get(member.email); if (!annotation) { return null; } if (annotation.isSupport) { - return cssMemberType('Grist support'); + return cssMemberType(t('Grist support')); } 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; if (!limit || !limit.top) { return null; } const elements: HTMLSpanElement[] = []; 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 { - elements.push(cssMemberTypeProblem(`${capitalizeFirstWord(collaborator)} limit exceeded`)); + elements.push(cssMemberTypeProblem( + t(`{{collaborator}} limit exceeded`, { collaborator: capitalizeFirstWord(collaborator) })) + ); } if (annotations.hasTeam) { // 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); } }), - `Add ${member.name || 'member'} to your team`)); + t(`Add {{member}} to your team`, { member: member.name || t('member') }))); } else if (limit.at >= limit.top) { 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; }); @@ -418,13 +426,13 @@ export class UserManager extends Disposable { let memberType: string; if (annotation.isSupport) { - memberType = 'Grist support'; + memberType = t('Grist support'); } else if (annotation.isMember && annotations.hasTeam) { - memberType = 'Team member'; + memberType = t('Team member'); } else if (annotations.hasTeam) { - memberType = 'Outside collaborator'; + memberType = t('Outside collaborator'); } else { - memberType = 'Collaborator'; + memberType = t('Collaborator'); } return cssMemberType(memberType, testId('um-member-annotation')); @@ -439,8 +447,8 @@ export class UserManager extends Disposable { cssMemberListItem( cssPublicMemberIcon('PublicFilled'), cssMemberText( - cssMemberPrimary('Public Access'), - cssMemberSecondary('Anyone with link ', makeCopyBtn(this._options.linkToCopy)), + cssMemberPrimary(t('Public Access')), + cssMemberSecondary(t('Anyone with link '), makeCopyBtn(this._options.linkToCopy)), ), this._memberRoleSelector(publicMember.effectiveAccess, publicMember.inheritedAccess, false, this._model.publicUserSelectOptions @@ -472,12 +480,12 @@ export class UserManager extends Disposable { cssMemberPrimary(name, testId('um-member-name')), activeUser?.email ? cssMemberSecondary(activeUser.email) : null, cssMemberPublicAccess( - dom('span', 'Public access', testId('um-member-annotation')), + dom('span', t('Public access'), testId('um-member-annotation')), cssPublicAccessIcon('PublicFilled'), ), ), cssRoleBtn( - accessLabel ?? 'Guest', + accessLabel ?? t('Guest'), cssCollapseIcon('Collapse'), dom.cls('disabled'), 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. - 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. dom.maybe((use) => use(inherited) && !isActiveUser, () => menuText( - `User inherits permissions from ${getResourceParent(this._model.resourceType)}. To remove, ` + - `set 'Inherit access' option to 'None'.`)), + t(`User inherits permissions from {{parent}}. To remove, \ +set 'Inherit access' option to 'None'.`, { parent: getResourceParent(this._model.resourceType) }))), // If the user is a guest, give a description of the guest permission. dom.maybe((use) => !this._model.isOrg && use(role) === roles.GUEST, () => menuText( - `User has view access to ${this._model.resourceType} resulting from manually-set access ` + - `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 ` + - `granted to individual documents or workspaces, rather than the full team site.`) : null + 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.`, + { resource: this._model.resourceType }))), + 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) => { // 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. const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(role) === _role.value); - return activeRole ? activeRole.label : "Guest"; + return activeRole ? activeRole.label : t("Guest"); }), cssCollapseIcon('Collapse'), this._model.isPersonal ? dom.cls('disabled') : null, @@ -634,7 +643,7 @@ function getFullUser(member: IEditableMember): FullUser { // Create a "Copy Link" button. 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)), testId('um-copy-link'), ...domArgs, @@ -646,7 +655,7 @@ function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[]) async function copyLink(elem: HTMLElement, link: string) { await copyToClipboard(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, @@ -808,9 +817,9 @@ const cssMemberPublicAccess = styled(cssMemberSecondary, ` function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) { switch (resourceType) { case 'organization': { - if (personal) { return 'Your role for this team site'; } + if (personal) { return t('Your role for this team site'); } return [ - 'Manage members of team site', + t('Manage members of team site'), !resource ? null : cssOrgName( `${(resource as Organization).name} (`, cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`), @@ -819,12 +828,14 @@ function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: ]; } 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. function resourceName(resourceType: ResourceType): string { - return resourceType === 'organization' ? 'team site' : resourceType; + return resourceType === 'organization' ? t('team site') : resourceType; } diff --git a/app/client/ui/WelcomeTour.ts b/app/client/ui/WelcomeTour.ts index e2128642..6cafb14d 100644 --- a/app/client/ui/WelcomeTour.ts +++ b/app/client/ui/WelcomeTour.ts @@ -10,7 +10,7 @@ import { dom, styled } from "grainjs"; const t = makeT('WelcomeTour'); -export const welcomeTour: IOnBoardingMsg[] = [ +export const WelcomeTour: IOnBoardingMsg[] = [ { title: t('Editing Data'), body: () => [ @@ -97,7 +97,7 @@ export const welcomeTour: IOnBoardingMsg[] = [ export function startWelcomeTour(onFinishCB: () => void) { commands.allCommands.fieldTabOpen.run(); - startOnBoarding(welcomeTour, onFinishCB); + startOnBoarding(WelcomeTour, onFinishCB); } const TopBarButtonIcon = styled(icon, ` diff --git a/app/client/ui/errorPages.ts b/app/client/ui/errorPages.ts index a6eea10d..0f989c88 100644 --- a/app/client/ui/errorPages.ts +++ b/app/client/ui/errorPages.ts @@ -35,8 +35,8 @@ export function createForbiddenPage(appModel: AppModel, message?: string) { return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [ dom.domComputed(appModel.currentValidUser, user => user ? [ 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 " + - "account, or ask an administrator for access.", {email: dom('b', user.email)})), + 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)})), ] : [ // 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 diff --git a/app/client/ui/searchDropdown.ts b/app/client/ui/searchDropdown.ts index 8597778c..4884ec7b 100644 --- a/app/client/ui/searchDropdown.ts +++ b/app/client/ui/searchDropdown.ts @@ -10,6 +10,9 @@ import { icon } from "app/client/ui2018/icons"; import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel"; import { mergeWith } from "lodash"; import { getOptionFull, SimpleList } from "../lib/simpleList"; +import { makeT } from 'app/client/lib/localization'; + +const t = makeT('searchDropdown'); const testId = makeTestId('test-sd-'); @@ -92,7 +95,7 @@ class DropdownWithSearch extends Disposable { cssMenuHeader( cssSearchIcon('Search'), this._inputElem = cssSearch( - {placeholder: this._options.placeholder || 'Search'}, + {placeholder: this._options.placeholder || t('Search')}, dom.on('input', () => { this._update(); }), dom.on('blur', () => setTimeout(() => this._inputElem.focus(), 0)), ), diff --git a/app/client/ui2018/search.ts b/app/client/ui2018/search.ts index acb72723..37a53340 100644 --- a/app/client/ui2018/search.ts +++ b/app/client/ui2018/search.ts @@ -177,7 +177,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) { cssTopBarBtn('Search', testId('icon'), dom.on('click', focusAndSelect), - hoverTooltip('Search', {key: 'topBarBtnTooltip'}), + hoverTooltip(t('Search'), {key: 'topBarBtnTooltip'}), ) ), expandedSearch( diff --git a/static/locales/en.client.json b/static/locales/en.client.json index a1504192..766dee29 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -39,7 +39,9 @@ "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.", - "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": { "API": "API", @@ -589,7 +591,8 @@ "Send to Google Drive": "Send to Google Drive", "Show in folder": "Show in folder", "Unsaved": "Unsaved", - "Work on a Copy": "Work on a Copy" + "Work on a Copy": "Work on a Copy", + "Share": "Share" }, "SiteSwitcher": { "Create new team site": "Create new team site", @@ -801,7 +804,8 @@ "Find Next ": "Find Next ", "Find Previous ": "Find Previous ", "No results": "No results", - "Search in document": "Search in document" + "Search in document": "Search in document", + "Search": "Search" }, "sendToDrive": { "Sending file to Google Drive": "Sending file to Google Drive" @@ -979,7 +983,9 @@ "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.", "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": { "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 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": { "Close": "Close", "Contribute": "Contribute",