From f8c6892643e7021d993aacb304e62d9615a3de8b Mon Sep 17 00:00:00 2001 From: Florent Date: Tue, 21 Nov 2023 21:20:40 +0100 Subject: [PATCH 1/8] OIDC allow to configure name and email attrs, and to skipp end session endpoint (#746) * support GRIST_OIDC_SP_PROFILE_NAME_ATTR, defaulting to the concatenation of "given_name" + "family_name" or the "name" attribute. * support GRIST_OIDC_SP_PROFILE_EMAIL_ATTR, defaulting to "email". * support GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: If set to "true", will not attempt to call the IdP's end_session_endpoint. Fail early if the endpoint does not exist, and this variable isn't set. The last part is because some IdPs like Gitlab do not provide end_session_endpoint. In such cases, GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true should be set to have the Grist logout button only log out of Grist, and not out of the IdP. --------- Co-authored-by: Florent FAYOLLE --- app/server/lib/OIDCConfig.ts | 61 ++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index b0889236..5c36d09c 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -6,8 +6,8 @@ * IdP is the "Identity Provider", somewhere users log into, e.g. Okta or Google Apps. * * We also use optional attributes for the user's name, for which we accept any of: - * given_name - * family_name + * given_name + family_name + * name * * Expected environment variables: * env GRIST_OIDC_SP_HOST=https:// @@ -21,6 +21,14 @@ * The client secret for the application, as registered with the IdP. * env GRIST_OIDC_IDP_SCOPES * The scopes to request from the IdP, as a space-separated list. Defaults to "openid email profile". + * env GRIST_OIDC_SP_PROFILE_NAME_ATTR + * The key of the attribute to use for the user's name. + * If omitted, the name will either be the concatenation of "given_name" + "family_name" or the "name" attribute. + * env GRIST_OIDC_SP_PROFILE_EMAIL_ATTR + * The key of the attribute to use for the user's email. Defaults to "email". + * env GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT + * If set to "true", on logout, there won't be any attempt to call the IdP's end_session_endpoint + * (the user will remain logged in in the IdP). * * This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions * at: @@ -43,12 +51,16 @@ import { Sessions } from './Sessions'; import log from 'app/server/lib/log'; import { appSettings } from './AppSettings'; import { RequestWithLogin } from './Authorizer'; +import { UserProfile } from 'app/common/LoginSessionAPI'; const CALLBACK_URL = '/oauth2/callback'; export class OIDCConfig { private _client: Client; private _redirectUrl: string; + private _namePropertyKey?: string; + private _emailPropertyKey: string; + private _skipEndSessionEndpoint: boolean; public constructor() { } @@ -69,6 +81,19 @@ export class OIDCConfig { envVar: 'GRIST_OIDC_IDP_CLIENT_SECRET', censor: true, }); + this._namePropertyKey = section.flag('namePropertyKey').readString({ + envVar: 'GRIST_OIDC_SP_PROFILE_NAME_ATTR', + }); + + this._emailPropertyKey = section.flag('emailPropertyKey').requireString({ + envVar: 'GRIST_OIDC_SP_PROFILE_EMAIL_ATTR', + defaultValue: 'email', + }); + + this._skipEndSessionEndpoint = section.flag('endSessionEndpoint').readBool({ + envVar: 'GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT', + defaultValue: false, + })!; const issuer = await Issuer.discover(issuerUrl); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; @@ -78,6 +103,10 @@ export class OIDCConfig { redirect_uris: [ this._redirectUrl ], response_types: [ 'code' ], }); + if (this._client.issuer.metadata.end_session_endpoint === undefined && !this._skipEndSessionEndpoint) { + throw new Error('The Identity provider does not propose end_session_endpoint. ' + + 'If that is expected, please set GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT=true'); + } log.info(`OIDCConfig: initialized with issuer ${issuerUrl}`); } @@ -140,6 +169,10 @@ export class OIDCConfig { } public async getLogoutRedirectUrl(req: express.Request, redirectUrl: URL): Promise { + // For IdPs that don't have end_session_endpoint, we just redirect to the logout page. + if (this._skipEndSessionEndpoint) { + return redirectUrl.href; + } return this._client.endSessionUrl({ post_logout_redirect_uri: redirectUrl.href }); @@ -167,14 +200,22 @@ export class OIDCConfig { return codeVerifier; } - private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse) { - const email = userInfo.email; - const fname = userInfo.given_name ?? ''; - const lname = userInfo.family_name ?? ''; - return { - email, - name: `${fname} ${lname}`.trim(), - }; + private _makeUserProfileFromUserInfo(userInfo: UserinfoResponse): Partial { + return { + email: String(userInfo[ this._emailPropertyKey ]), + name: this._extractName(userInfo) + + }; + } + + private _extractName(userInfo: UserinfoResponse): string|undefined { + if (this._namePropertyKey) { + return (userInfo[ this._namePropertyKey ] as any)?.toString(); + } + const fname = userInfo.given_name ?? ''; + const lname = userInfo.family_name ?? ''; + + return `${fname} ${lname}`.trim() || userInfo.name; } } From 570e4032a416a8442d329dbe3551208a658fd6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C4=8Dek=20Prijatelj?= Date: Mon, 20 Nov 2023 20:45:51 +0000 Subject: [PATCH 2/8] Translated using Weblate (Slovenian) Currently translated at 100.0% (999 of 999 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sl/ --- static/locales/sl.client.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/static/locales/sl.client.json b/static/locales/sl.client.json index 636d4d9e..e69bfb5f 100644 --- a/static/locales/sl.client.json +++ b/static/locales/sl.client.json @@ -146,7 +146,7 @@ "Deleted {{at}}": "Izbrisano {{at}}", "Delete {{name}}": "Izbriši {{name}}", "Document will be permanently deleted.": "Dokument bo trajno izbrisan.", - "Permanently Delete \"{{name}}\"?": "Trajno izbrisati \"{{name}}\"?", + "Permanently Delete \"{{name}}\"?": "Trajno izbrišem \"{{name}}\"?", "(The organization needs a paid plan)": "(Organizacija potrebuje plačljiv načrt)", "Access Details": "Podrobnosti o dostopu", "All Documents": "Vsi dokumenti", @@ -168,7 +168,7 @@ "Requires edit permissions": "Zahteva dovoljenja za urejanje", "Other Sites": "Druga spletna mesta", "Pinned Documents": "Pripeti dokumenti", - "To restore this document, restore the workspace first.": "Če želite obnoviti ta dokument, najprej obnovite delovni prostor.", + "To restore this document, restore the workspace first.": "Če želiš obnoviti ta dokument, najprej obnovi delovni prostor.", "You are on your personal site. You also have access to the following sites:": "Nahajate se na svojem osebnem spletnem mestu. Prav tako imate dostop do naslednjih spletnih mest:", "Restore": "Obnovi", "Move {{name}} to workspace": "Premakni {{name}} v delovni prostor", @@ -590,7 +590,7 @@ "Organization": "Organizacija", "Replacing the original requires editing rights on the original document.": "Za zamenjavo izvirnika so potrebne pravice za urejanje izvirnega dokumenta.", "Remove document history (can significantly reduce file size)": "Odstranitev zgodovine dokumenta (lahko znatno zmanjša velikost datoteke)", - "To save your changes, please sign up, then reload this page.": "Če želite shraniti spremembe, se prijavite in nato ponovno naložite to stran.", + "To save your changes, please sign up, then reload this page.": "Če želiš shraniti spremembe, se prijavi in nato ponovno naloži to stran.", "The original version of this document will be updated.": "Prvotna različica tega dokumenta bo posodobljena.", "However, it appears to be already identical.": "Vendar se zdi, da je že identična.", "Update Original": "Posodobitev izvirnika", @@ -642,7 +642,7 @@ "Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Pravila dostopa vam omogočajo, da ustvarite podrobna pravila, s katerimi določite, kdo lahko vidi ali ureja posamezne dele dokumenta.", "Rearrange the fields in your card by dragging and resizing cells.": "Z vlečenjem in spreminjanjem velikosti polj na kartici spremenite njihovo razporeditev.", "Useful for storing the timestamp or author of a new record, data cleaning, and more.": "Uporabno za shranjevanje časovnega žiga ali avtorja novega zapisa, čiščenje podatkov in drugo.", - "Click the Add New button to create new documents or workspaces, or import data.": "Če želite ustvariti nove dokumente ali delovne prostore ali uvoziti podatke, kliknite gumb Dodaj.", + "Click the Add New button to create new documents or workspaces, or import data.": "Klikni gumb dodaj novega, če želiš ustvariti nove dokumente, delovne prostore ali uvoziti podatke,", "Nested Filtering": "Vgnezdeno filtriranje", "Only those rows will appear which match all of the filters.": "Prikazane bodo samo tiste vrstice, ki ustrezajo vsem filtrom.", "Editing Card Layout": "Urejanje postavitve kartice", @@ -655,11 +655,11 @@ "They allow for one record to point (or refer) to another.": "Omogočajo, da en zapis kaže (ali se sklicuje) na drugega.", "Reference Columns": "Referenčni stolpci", "To configure your calendar, select columns for start": { - "end dates and event titles. Note each column's type.": "Če želite konfigurirati svoj koledar, izberite stolpce za začetne/končne datume in naslove dogodkov. Upoštevajte vrsto vsakega stolpca." + "end dates and event titles. Note each column's type.": "Če želiš konfigurirati svoj koledar, izberi stolpce za začetne/končne datume in naslove dogodkov. Upoštevaj vrsto vsakega stolpca." }, "Calendar": "Koledar", "Apply conditional formatting to cells in this column when formula conditions are met.": "Uporabi pogojno oblikovanje za celice v tem stolpcu, ko so izpolnjeni pogoji formule.", - "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Če želite narediti sidrno povezavo, ki uporabnika pripelje do določene celice, kliknite vrstico in pritisnite {{shortcut}}.", + "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "Če želiš narediti sidrno povezavo, ki uporabnika pripelje do določene celice, klikni vrstico in pritisni {{shortcut}}.", "Unpin to hide the the button while keeping the filter.": "Odpnite, da skrijete gumb in obdržite filter.", "Apply conditional formatting to rows based on formulas.": "Uporabi pogojno oblikovanje za vrstice na podlagi formul.", "Click on “Open row styles” to apply conditional formatting to rows.": "Klikni »Odpri sloge vrstic«, da za vrstice uporabiš pogojno oblikovanje.", @@ -707,14 +707,14 @@ "Close": "Zapri", "Allow anyone with the link to open.": "Omogočite odprtje vsakomur, ki ima povezavo.", "Invite people to {{resourceType}}": "Povabite ljudi k {{resourceType}}", - "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želite odstraniti, nastavite možnost \"Podeduje dostop\" na \"Ne\".", + "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želiš odstraniti, nastavi možnost \"Podeduje dostop\" na \"Ne\".", "Remove my access": "Odstranitev mojega dostopa", "Public access": "Javni dostop", "Public Access": "Javni dostop", "Cancel": "Prekliči", "Grist support": "Grist podpora", "You are about to remove your own access to this {{resourceType}}": "Odstranili boste svoj dostop do tega {{resourceType}}", - "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želite odstraniti, nastavite možnost \"Podeduje dostop\" na \"Ni\".", + "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Uporabnik podeduje dovoljenja od {{parent}}. Če jih želiš odstraniti, nastavi možnost \"Podeduje dostop\" na \"Ni\".", "Guest": "Gost", "Invite multiple": "Povabite več", "Confirm": "Potrdite", @@ -813,7 +813,7 @@ "Welcome to Grist!": "Dobrodošli v Gristu!", "Visit our {{link}} to learn more about Grist.": "Obiščite našo spletno stran {{link}} da izveste več o Grisstu.", "Sign in": "Prijavi se", - "To use Grist, please either sign up or sign in.": "Če želite uporabljati Grist, se prijavite ali prvič prijavite." + "To use Grist, please either sign up or sign in.": "Če želiš uporabljati Grist, se prijavi ali prvič prijavi." }, "WelcomeSitePicker": { "You have access to the following Grist sites.": "Imate dostop do naslednjih Grist spletnih mest .", From 1fec674d28f2c6abe26710d4eb60c7eaae9c0e91 Mon Sep 17 00:00:00 2001 From: Florent Date: Wed, 22 Nov 2023 20:58:29 +0100 Subject: [PATCH 3/8] OIDC: ensure that email_veridied is set by default (#765) Co-authored-by: Florent FAYOLLE --- app/server/lib/OIDCConfig.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/server/lib/OIDCConfig.ts b/app/server/lib/OIDCConfig.ts index 5c36d09c..58d01443 100644 --- a/app/server/lib/OIDCConfig.ts +++ b/app/server/lib/OIDCConfig.ts @@ -29,6 +29,9 @@ * env GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT * If set to "true", on logout, there won't be any attempt to call the IdP's end_session_endpoint * (the user will remain logged in in the IdP). + * env GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED + * If set to "true", the user will be allowed to login even if the email is not verified by the IDP. + * Defaults to false. * * This version of OIDCConfig has been tested with Keycloak OIDC IdP following the instructions * at: @@ -61,6 +64,7 @@ export class OIDCConfig { private _namePropertyKey?: string; private _emailPropertyKey: string; private _skipEndSessionEndpoint: boolean; + private _ignoreEmailVerified: boolean; public constructor() { } @@ -95,6 +99,11 @@ export class OIDCConfig { defaultValue: false, })!; + this._ignoreEmailVerified = section.flag('ignoreEmailVerified').readBool({ + envVar: 'GRIST_OIDC_SP_IGNORE_EMAIL_VERIFIED', + defaultValue: false, + })!; + const issuer = await Issuer.discover(issuerUrl); this._redirectUrl = new URL(CALLBACK_URL, spHost).href; this._client = new issuer.Client({ @@ -134,6 +143,11 @@ export class OIDCConfig { ); const userInfo = await this._client.userinfo(tokenSet); + + if (!this._ignoreEmailVerified && userInfo.email_verified !== true) { + throw new Error(`OIDCConfig: email not verified for ${userInfo.email}`); + } + const profile = this._makeUserProfileFromUserInfo(userInfo); log.info(`OIDCConfig: got OIDC response for ${profile.email} (${profile.name}) redirecting to ${targetUrl}`); @@ -204,7 +218,6 @@ export class OIDCConfig { return { email: String(userInfo[ this._emailPropertyKey ]), name: this._extractName(userInfo) - }; } From 9e37fe83b53e2cb8b6e414e4d6164a14cf1b83d6 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Wed, 22 Nov 2023 17:46:25 +0000 Subject: [PATCH 4/8] Translated using Weblate (Portuguese (Brazil)) Currently translated at 100.0% (1012 of 1012 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/pt_BR/ --- static/locales/pt_BR.client.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/static/locales/pt_BR.client.json b/static/locales/pt_BR.client.json index dce820bd..735e209c 100644 --- a/static/locales/pt_BR.client.json +++ b/static/locales/pt_BR.client.json @@ -217,7 +217,13 @@ "Duplicate Table": "Duplicar a Tabela", "Raw Data Tables": "Tabelas de Dados Primários", "Table ID copied to clipboard": "ID da Tabela copiada para a área de transferência", - "You do not have edit access to this document": "Você não tem permissão de edição desse documento" + "You do not have edit access to this document": "Você não tem permissão de edição desse documento", + "Edit Record Card": "Editar cartão de registro", + "Rename Table": "Renomear tabela", + "{{action}} Record Card": "{{action}} Cartão de registro", + "Record Card": "Cartão de registro", + "Remove Table": "Remover tabela", + "Record Card Disabled": "Cartão de registro desabilitado" }, "DocHistory": { "Activity": "Atividade", @@ -663,7 +669,8 @@ "Insert row above": "Inserir linha acima", "Insert row below": "Inserir linha abaixo", "Duplicate rows_one": "Duplicar linha", - "Duplicate rows_other": "Duplicar linhas" + "Duplicate rows_other": "Duplicar linhas", + "View as card": "Ver como cartão" }, "SelectionSummary": { "Copied to clipboard": "Copiado para a área de transferência" @@ -1295,5 +1302,13 @@ "FloatingPopup": { "Maximize": "Maximizar", "Minimize": "Minimizar" + }, + "CardContextMenu": { + "Insert card above": "Inserir cartão acima", + "Duplicate card": "Duplicar o cartão", + "Insert card below": "Inserir cartão abaixo", + "Delete card": "Excluir cartão", + "Copy anchor link": "Copiar link de ancoragem", + "Insert card": "Inserir cartão" } } From 8dfbfd8217fc3417fe466511f9918cbf74a886ff Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Tue, 21 Nov 2023 21:30:07 +0000 Subject: [PATCH 5/8] Translated using Weblate (Spanish) Currently translated at 100.0% (1012 of 1012 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/es/ --- static/locales/es.client.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/static/locales/es.client.json b/static/locales/es.client.json index 252b53f2..221fa367 100644 --- a/static/locales/es.client.json +++ b/static/locales/es.client.json @@ -543,7 +543,8 @@ "Insert row above": "Insertar fila arriba", "Insert row below": "Insertar fila debajo", "Duplicate rows_one": "Duplicar fila", - "Duplicate rows_other": "Duplicar filas" + "Duplicate rows_other": "Duplicar filas", + "View as card": "Ver como tarjeta" }, "ShareMenu": { "Access Details": "Detalles de Acceso", @@ -718,7 +719,13 @@ "Duplicate Table": "Duplicar tabla", "Raw Data Tables": "Tablas de datos brutos", "Table ID copied to clipboard": "ID de tabla copiado al portapapeles", - "Click to copy": "Haga clic para copiar" + "Click to copy": "Haga clic para copiar", + "Edit Record Card": "Editar la ficha del registro", + "Rename Table": "Cambiar el nombre de la tabla", + "{{action}} Record Card": "{{action}} Ficha", + "Record Card": "Ficha de registro", + "Remove Table": "Quitar la tabla", + "Record Card Disabled": "Tarjeta de registro desactivada" }, "DocPageModel": { "Add Empty Table": "Agregar tabla vacía", @@ -1285,5 +1292,13 @@ "FloatingPopup": { "Maximize": "Maximizar", "Minimize": "Minimizar" + }, + "CardContextMenu": { + "Insert card above": "Inserte la tarjeta arriba", + "Duplicate card": "Tarjeta duplicada", + "Insert card below": "Inserte la tarjeta a continuación", + "Delete card": "Borrar la tarjeta", + "Copy anchor link": "Copiar enlace fijado", + "Insert card": "Insertar la tarjeta" } } From 9e6f3775c379f97963c14bc9dbe4ddc28c2a3864 Mon Sep 17 00:00:00 2001 From: Paul Janzen Date: Wed, 22 Nov 2023 17:44:45 +0000 Subject: [PATCH 6/8] Translated using Weblate (German) Currently translated at 100.0% (1012 of 1012 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/de/ --- static/locales/de.client.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/static/locales/de.client.json b/static/locales/de.client.json index b320802a..a7546d2d 100644 --- a/static/locales/de.client.json +++ b/static/locales/de.client.json @@ -217,7 +217,13 @@ "Duplicate Table": "Tabelle duplizieren", "Raw Data Tables": "Rohdaten-Tabellen", "Table ID copied to clipboard": "Tabellen-ID in die Zwischenablage kopiert", - "You do not have edit access to this document": "Sie haben keinen Bearbeitungszugriff auf dieses Dokument" + "You do not have edit access to this document": "Sie haben keinen Bearbeitungszugriff auf dieses Dokument", + "Edit Record Card": "Karteikarte bearbeiten", + "Rename Table": "Tabelle umbenennen", + "{{action}} Record Card": "{{action}} Karteikarte", + "Record Card": "Karteikarte", + "Remove Table": "Tabelle entfernen", + "Record Card Disabled": "Karteikarte Deaktiviert" }, "DocHistory": { "Activity": "Aktivität", @@ -663,7 +669,8 @@ "Insert row above": "Zeile oben einfügen", "Insert row below": "Zeile unten einfügen", "Duplicate rows_one": "Zeile duplizieren", - "Duplicate rows_other": "Zeilen duplizieren" + "Duplicate rows_other": "Zeilen duplizieren", + "View as card": "Ansicht als Karte" }, "SelectionSummary": { "Copied to clipboard": "In die Zwischenablage kopiert" @@ -1295,5 +1302,13 @@ }, "searchDropdown": { "Search": "Suchen" + }, + "CardContextMenu": { + "Insert card above": "Karte oben einfügen", + "Duplicate card": "Karte duplizieren", + "Insert card below": "Karte unten einfügen", + "Delete card": "Karte löschen", + "Copy anchor link": "Ankerlink kopieren", + "Insert card": "Karte einfügen" } } From c06828d35da0a8fe316aca14be508e3d4be886f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C4=8Dek=20Prijatelj?= Date: Wed, 22 Nov 2023 17:56:04 +0000 Subject: [PATCH 7/8] Translated using Weblate (Slovenian) Currently translated at 100.0% (1012 of 1012 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/sl/ --- static/locales/sl.client.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/static/locales/sl.client.json b/static/locales/sl.client.json index e69bfb5f..bf76dc00 100644 --- a/static/locales/sl.client.json +++ b/static/locales/sl.client.json @@ -260,7 +260,8 @@ "Copy anchor link": "Kopiraj sidrno povezavo", "Duplicate rows_one": "Podvoji vrstico", "Duplicate rows_other": "Podvoji vrstice", - "Insert row above": "Vstavi vrstico zgoraj" + "Insert row above": "Vstavi vrstico zgoraj", + "View as card": "Kartični pogled" }, "Tools": { "Delete": "Izbriši", @@ -298,7 +299,13 @@ "Duplicate Table": "Podvojena tabela", "Table ID copied to clipboard": "ID tabele kopiran v odložišče", "You do not have edit access to this document": "Nimate dostopa za urejanje tega dokumenta", - "Raw Data Tables": "Neobdelana tabela" + "Raw Data Tables": "Neobdelana tabela", + "Edit Record Card": "Uredi evidenčno kartico", + "Rename Table": "Preimenuj Tabelo", + "{{action}} Record Card": "{{action}} Evidenčno Kartico", + "Record Card": "Evidenčna kartica", + "Remove Table": "Odstrani Tabelo", + "Record Card Disabled": "Evidenčna kartica onemogočena" }, "ViewLayoutMenu": { "Delete record": "Brisanje zapisa", @@ -1231,5 +1238,13 @@ }, "sendToDrive": { "Sending file to Google Drive": "Pošiljanje datoteke v Google Drive" + }, + "CardContextMenu": { + "Insert card above": "Vstavi kartico zgoraj", + "Duplicate card": "Podvoji kartico", + "Insert card below": "Vstavi kartico spodaj", + "Delete card": "Briši kartico", + "Copy anchor link": "Kopiraj sidrno povezavo", + "Insert card": "Vstavi kartico" } } From b21baba128a5147a4da1504f75ee0d1e5d4debde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=BB=D0=B0=D0=B4=D0=B8=D0=BC=D0=B8=D1=80=20=D0=92?= Date: Thu, 23 Nov 2023 19:57:09 +0000 Subject: [PATCH 8/8] Translated using Weblate (Russian) Currently translated at 99.6% (1008 of 1012 strings) Translation: Grist/client Translate-URL: https://hosted.weblate.org/projects/grist/client/ru/ --- static/locales/ru.client.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/static/locales/ru.client.json b/static/locales/ru.client.json index dfb3bf6b..2d0f686d 100644 --- a/static/locales/ru.client.json +++ b/static/locales/ru.client.json @@ -334,7 +334,13 @@ "Duplicate Table": "Дублировать таблицу", "Table ID copied to clipboard": "Идентификатор таблицы скопирован в буфер обмена", "You do not have edit access to this document": "У вас нет доступа к редактированию этого документа", - "Delete {{formattedTableName}} data, and remove it from all pages?": "Удалить {{formattedTableName}} данные, и удалить их со всех страниц?" + "Delete {{formattedTableName}} data, and remove it from all pages?": "Удалить {{formattedTableName}} данные, и удалить их со всех страниц?", + "Edit Record Card": "Редактировать карточку записи", + "Rename Table": "Переименовать Таблицу", + "{{action}} Record Card": "{{action}} Карточка записи", + "Record Card": "Карточка записи", + "Remove Table": "Удалить Таблицу", + "Record Card Disabled": "Карточка записи отключена" }, "DocHistory": { "Snapshots": "Снимки", @@ -603,7 +609,8 @@ "Insert row below": "Вставить строку ниже", "Insert row": "Вставить строку", "Insert row above": "Вставить строку выше", - "Delete": "Удалить" + "Delete": "Удалить", + "View as card": "Посмотреть как карточку" }, "RecordLayout": { "Updating record layout.": "Обновление макета записи." @@ -1231,5 +1238,13 @@ "FloatingPopup": { "Maximize": "Максимизировать", "Minimize": "Минимизировать" + }, + "CardContextMenu": { + "Insert card above": "Вставить карточку выше", + "Duplicate card": "Дублировать карточку", + "Insert card below": "Вставить карточку ниже", + "Delete card": "Удалить карточку", + "Copy anchor link": "Скопировать якорную ссылку", + "Insert card": "Вставить карточку" } }