diff --git a/.github/workflows/integration-testing.yaml b/.github/workflows/integration-testing.yaml
index fb9b9cf2eb..887dd2f73f 100644
--- a/.github/workflows/integration-testing.yaml
+++ b/.github/workflows/integration-testing.yaml
@@ -8,7 +8,7 @@ concurrency: integration_environment
jobs:
variables:
- if: ${{ github.event_name == 'merge_group' }}
+ if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
outputs:
date: ${{ steps.data.outputs.date }}
@@ -29,7 +29,7 @@ jobs:
echo "current_branch=merge-queue"
>> $GITHUB_OUTPUT
build-generic:
- if: ${{ github.event_name == 'merge_group' }}
+ if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}
name: "Integration Image Build"
needs:
- variables
@@ -83,7 +83,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-derived:
- if: ${{ github.event_name == 'merge_group' }}
+ if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
name: "Integration Image Build Stage 2"
permissions:
@@ -140,7 +140,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
run-tests:
- if: ${{ github.event_name == 'merge_group' }}
+ if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}
name: "Playwright" # This name is referenced when slacking status
needs:
- build-derived
@@ -204,7 +204,7 @@ jobs:
- name: Uninstall
run: helm delete integration-${{ needs.variables.outputs.commit }} -n ${{ secrets.DEV_SANDBOX_NAMESPACE }} --debug --timeout 10m0s
ending-notification:
- if: ${{ github.event_name == 'merge_group' }}
+ if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }}
runs-on: ubuntu-latest
needs:
- run-tests
diff --git a/e2e-tests/globals.ts b/e2e-tests/globals.ts
index a4b53698f2..959dd8b4a6 100644
--- a/e2e-tests/globals.ts
+++ b/e2e-tests/globals.ts
@@ -5,6 +5,12 @@ export const LANGUAGES = {
'HE': 'hebrew',
}
+export const SOURCE_LANGUAGES = {
+ 'EN': /^(תרגום|Translation)$/,
+ 'HE': /^(מקור|Source)$/,
+ 'BI': /^(מקור ותרגום|Source with Translation)$/
+}
+
export const cookieObject = {
"name": "interfaceLang",
"value": DEFAULT_LANGUAGE,
diff --git a/e2e-tests/tests/interface-language-is-sticky.spec.ts b/e2e-tests/tests/interface-language-is-sticky.spec.ts
new file mode 100644
index 0000000000..682d48ee93
--- /dev/null
+++ b/e2e-tests/tests/interface-language-is-sticky.spec.ts
@@ -0,0 +1,57 @@
+import {test, expect} from '@playwright/test';
+import {changeLanguageOfText, goToPageWithLang, isIsraelIp} from "../utils";
+import {LANGUAGES, SOURCE_LANGUAGES} from '../globals'
+
+const interfaceTextHE = 'מקורות';
+const interfaceTextEN = 'Texts';
+
+[
+ // Hebrew Interface and English Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN,
+ expectedSourceText: 'When God began to create', expectedBilingualText: '', expectedInterfaceText: interfaceTextHE },
+
+ // Hebrew Interface and Bilingual Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI,
+ expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: 'When God began to create', expectedInterfaceText: interfaceTextHE },
+
+ // Hebrew Interface and Hebrew Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE,
+ expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: '', expectedInterfaceText: interfaceTextHE },
+
+ // English Interface and English Source
+ {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN,
+ expectedSourceText: 'When God began to create', expectedBilingualText: '', expectedInterfaceText: interfaceTextEN },
+
+ // English Interface and Bilingual Source
+ {interfaceLanguage: 'English', sinterfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI,
+ expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: 'When God began to create',
+ expectedInterfaceText: interfaceTextEN },
+
+ // English Interface and Hebrew Source
+ {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE,
+ expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: '', expectedInterfaceText: interfaceTextEN }
+
+].forEach(({interfaceLanguage, interfaceLanguageToggle, sourceLanguage, sourceLanguageToggle, expectedSourceText, expectedBilingualText, expectedInterfaceText}) => {
+ test(`${interfaceLanguage} Interface Language with ${sourceLanguage} Source`, async ({ context }) => {
+
+ // Navigating to Bereshit with selected Interface Language, Hebrew or English
+ const page = await goToPageWithLang(context,'/Genesis.1',`${interfaceLanguageToggle}`)
+
+ // Selecting Source Language
+ await changeLanguageOfText(page, sourceLanguageToggle)
+
+ // Locating the source text segment, then verifying translation
+ await expect(page.locator('div.segmentNumber').first().locator('..').locator('p')).toContainText(`${expectedSourceText}`)
+
+ // Validate Hebrew interface language is still toggled
+ const textLink = page.locator('a.textLink').first()
+ await expect(textLink).toHaveText(`${expectedInterfaceText}`)
+
+ })
+})
\ No newline at end of file
diff --git a/e2e-tests/tests/reader.spec.ts b/e2e-tests/tests/reader.spec.ts
index 4938ec0f2a..8236e263ac 100644
--- a/e2e-tests/tests/reader.spec.ts
+++ b/e2e-tests/tests/reader.spec.ts
@@ -15,14 +15,14 @@ test('Navigate to bereshit', async ({ context }) => {
});
test('Verify translations', async ({ context }) => {
- const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he');
+ const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he');
await page.getByRole('link', { name: 'Translations (4)' }).click();
await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' });
page.getByText('A. Cohen, Cambridge University Press, 1921', { exact: true })
});
test('Get word description', async ({ context }) => {
- const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he');
+ const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he');
await page.getByRole('link', { name: 'ר\' נחוניא בן הקנה' }).click();
await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' });
await page.getByText('Looking up words...').waitFor({ state: 'detached' });
@@ -31,11 +31,11 @@ test('Get word description', async ({ context }) => {
test('Open panel window', async ({ context }) => {
- const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he');
+ const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&&lang=bi&with=all&lang2=he');
await page.getByText('ולית הלכתא לא כרב הונא ולא כריב"ל כרב הונא הא דאמרן כריב"ל דאריב"ל כיון שהגיע זמ').click();
await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' });
await page.getByRole('link', { name: 'תלמוד (1)' }).click();
- await page.getByRole('link', { name: 'שבת (1) מלאכות האסורות בשבת ודינים הקשורים לקדושת היום.' }).click();
+ await page.getByRole('link', { name: /^שבת/ }).click();
await page.getByText('טעינה...').waitFor({ state: 'detached' });
await page.getByRole('link', { name: 'Open' }).click();
await page.getByRole('heading', { name: 'Loading...' }).getByText('Loading...').waitFor({ state: 'detached' });
diff --git a/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts b/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts
new file mode 100644
index 0000000000..46bb51ce58
--- /dev/null
+++ b/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts
@@ -0,0 +1,80 @@
+import {test, expect} from '@playwright/test';
+import {goToPageWithLang, changeLanguageOfText} from "../utils";
+import {LANGUAGES, SOURCE_LANGUAGES} from '../globals'
+
+[
+ // Hebrew Interface and Hebrew Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE,
+ translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'},
+
+ // Hebrew Interface and Bilingual Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI,
+ translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'},
+
+ // Hebrew Interface and English Source
+ {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE,
+ sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN,
+ translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'},
+
+ // English Interface and English Source
+ {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN,
+ translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'},
+
+ // English Interface and English Source
+ {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI,
+ translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'},
+
+ // English Interface and Hebrew Source
+ {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN,
+ sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE,
+ translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'}
+
+].forEach(({interfaceLanguage, interfaceLanguageToggle, sourceLanguage, sourceLanguageToggle, translations, currentlySelected, select}) => {
+ test(`${interfaceLanguage} - translation name appears in title for ${sourceLanguage} source text`, async ({ context }) => {
+ // Navigate to Bereshit in specified Interface Language
+ const page = await goToPageWithLang(context,'/Genesis.1', `${interfaceLanguageToggle}`)
+
+ // Change the Source Language of the text
+ await changeLanguageOfText(page, sourceLanguageToggle)
+
+ // Retain the translation name locator
+ const translationNameInTitle = page.locator('span.readerTextVersion')
+
+ // Navigate to the Translations sidebar by clicking on the text title
+ //Clicks on בראשית א׳ / Genesis I
+ await page.locator('h1').click()
+
+ // Click on Translations
+ await page.getByRole('link', {name: `${translations}`}).click()
+
+ // Wait for Translations side-bar to load by waiting for 'Translations' header
+ await page.waitForSelector('h3')
+
+ // Check if the default translation in the title matches the selected translation
+ // NOTE: We are skipping checking for the default translation here, due to the Hebrew text being default Masoretic
+ if(sourceLanguage !== 'Hebrew'){
+ const defaultTranslation = await translationNameInTitle.textContent()
+ await expect(page.locator('div.version-with-preview-title-line', {hasText: defaultTranslation!}).getByRole('link')).toHaveText(`${currentlySelected}`)
+ }
+
+ // TODO: 4th translation, handling Hebrew Interface translations in Hebrew. For example: 'חומש רש״י, רבי שרגא זילברשטיין' should appear in the translation title as written.
+ const translationNames = ['The Schocken Bible, Everett Fox, 1995 ©', '«Да» project']
+
+ // Utilizing the traditional for-loop as there are async issues with foreach
+ for(let i = 0; i < translationNames.length; i++){
+
+ // "Select" another translation.
+ await page.locator('div.version-with-preview-title-line', {hasText: translationNames[i]}).getByText(`${select}`).click()
+
+ // Validate selected translation is reflected in title
+ await expect(translationNameInTitle).toHaveText(translationNames[i])
+
+ // Validate selected translation says 'Currently Selected'
+ await expect(page.locator('div.version-with-preview-title-line', {hasText: translationNames[i]}).getByRole('link')).toHaveText(`${currentlySelected}`)
+ }
+ })
+});
\ No newline at end of file
diff --git a/e2e-tests/utils.ts b/e2e-tests/utils.ts
index dc3922d64e..22d75a0270 100644
--- a/e2e-tests/utils.ts
+++ b/e2e-tests/utils.ts
@@ -1,4 +1,4 @@
-import {DEFAULT_LANGUAGE, LANGUAGES, testUser} from './globals'
+import {DEFAULT_LANGUAGE, LANGUAGES, SOURCE_LANGUAGES, testUser} from './globals'
import {BrowserContext} from 'playwright-core';
import type { Page } from 'playwright-core';
@@ -23,13 +23,16 @@ export const changeLanguage = async (page: Page, language: string) => {
}
export const goToPageWithLang = async (context: BrowserContext, url: string, language=DEFAULT_LANGUAGE) => {
- if (!langCookies.length) {
- const page: Page = await context.newPage();
- await page.goto('');
- await changeLanguage(page, language);
- langCookies = await context.cookies();
- }
+ // If a cookie already has contents, clear it so that the language cookie can be reset
+ if (langCookies.length) {
+ await context.clearCookies()
+ }
+ const page: Page = await context.newPage();
+ await page.goto('');
+ await changeLanguage(page, language);
+ langCookies = await context.cookies();
await context.addCookies(langCookies);
+
// this is a hack to get the cookie to work
const newPage: Page = await context.newPage();
await newPage.goto(url);
@@ -65,4 +68,26 @@ export const goToPageWithUser = async (context: BrowserContext, url: string, use
export const getPathAndParams = (url: string) => {
const urlObj = new URL(url);
return urlObj.pathname + urlObj.search;
+}
+
+export const changeLanguageOfText = async (page: Page, sourceLanguage: RegExp) => {
+ // Clicking on the Source Language toggle
+ await page.getByAltText('Toggle Reader Menu Display Settings').click()
+
+ // Selecting Source Language
+ await page.locator('div').filter({ hasText: sourceLanguage }).click()
+}
+
+export const getCountryByIp = async (page: Page) => {
+ const data = await page.evaluate(() => {
+ return fetch('https://ipapi.co/json/')
+ .then(response => response.json())
+ .then(data => data)
+ })
+ return data.country;
+}
+
+export const isIsraelIp = async (page: Page) => {
+ const country = await getCountryByIp(page);
+ return country === "IL";
}
\ No newline at end of file
diff --git a/reader/views.py b/reader/views.py
index df04826270..3b8946893e 100644
--- a/reader/views.py
+++ b/reader/views.py
@@ -35,6 +35,7 @@
from sefaria.model import *
from sefaria.google_storage_manager import GoogleStorageManager
+from sefaria.model.text_reuqest_adapter import TextRequestAdapter
from sefaria.model.user_profile import UserProfile, user_link, public_user_data, UserWrapper
from sefaria.model.collection import CollectionSet
from sefaria.model.webpage import get_webpages_for_ref
@@ -250,6 +251,18 @@ def user_credentials(request):
return {"user_type": "API", "user_id": apikey["uid"]}
+def _reader_redirect_add_languages(request, tref):
+ versions = Ref(tref).version_list()
+ query_params = QueryDict(mutable=True)
+ for vlang, direction in [('ven', 'ltr'), ('vhe', 'rtl')]:
+ version_title = request.GET.get(vlang)
+ if version_title:
+ version_title = version_title.replace('_', ' ')
+ version = next((v for v in versions if v['direction'] == direction and v['versionTitle'] == version_title))
+ query_params[vlang] = f'{version["languageFamilyName"]}|{version["versionTitle"]}'
+ return redirect(f'/{tref}/?{urllib.parse.urlencode(query_params)}')
+
+
@ensure_csrf_cookie
def catchall(request, tref, sheet=None):
"""
@@ -265,6 +278,10 @@ def reader_redirect(uref):
response['Location'] += "?%s" % params if params else ""
return response
+ for version in ['ven', 'vhe']:
+ if request.GET.get(version) and '|' not in request.GET.get(version):
+ return _reader_redirect_add_languages(request, tref)
+
if sheet is None:
try:
oref = Ref.instantiate_ref_with_legacy_parse_fallback(tref)
@@ -290,7 +307,7 @@ def old_versions_redirect(request, tref, lang, version):
def get_connections_mode(filter):
# List of sidebar modes that can function inside a URL parameter to open the sidebar in that state.
- sidebarModes = ("Sheets", "Notes", "About", "AboutSheet", "Navigation", "Translations", "Translation Open","WebPages", "extended notes", "Topics", "Torah Readings", "manuscripts", "Lexicon", "SidebarSearch", "Guide")
+ sidebarModes = ("Sheets", "Notes", "About", "AboutSheet", "Navigation", "Translations", "Translation Open", "Version Open", "WebPages", "extended notes", "Topics", "Torah Readings", "manuscripts", "Lexicon", "SidebarSearch", "Guide")
if filter[0] in sidebarModes:
return filter[0], True
elif filter[0].endswith(" ConnectionsList"):
@@ -300,7 +317,7 @@ def get_connections_mode(filter):
else:
return "TextList", False
-def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **kwargs):
+def make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, mode, **kwargs):
"""
Returns a dictionary corresponding to the React panel state,
additionally setting `text` field with textual content.
@@ -308,15 +325,8 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k
if oref.is_book_level():
index_details = library.get_index(oref.normal()).contents(with_content_counts=True)
index_details["relatedTopics"] = get_topics_for_book(oref.normal(), annotate=True)
- if kwargs.get('extended notes', 0) and (versionEn is not None or versionHe is not None):
- currVersions = {"en": versionEn, "he": versionHe}
- if versionEn is not None and versionHe is not None:
- curr_lang = kwargs.get("panelDisplayLanguage", "en")
- for key in list(currVersions.keys()):
- if key == curr_lang:
- continue
- else:
- currVersions[key] = None
+ if kwargs.get('extended notes', 0):
+ currVersions = {"en": translationVersion, "he": primaryVersion}
panel = {
"menuOpen": "extended notes",
"mode": "Menu",
@@ -339,10 +349,7 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k
"mode": mode,
"ref": oref.normal(),
"refs": [oref.normal()] if not oref.is_spanning() else [r.normal() for r in oref.split_spanning_ref()],
- "currVersions": {
- "en": versionEn,
- "he": versionHe,
- },
+ "currVersions": {"en": translationVersion, "he": primaryVersion},
"filter": filter,
"versionFilter": versionFilter,
}
@@ -373,15 +380,29 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k
if settings_override:
panel["settings"] = settings_override
if mode != "Connections" and oref != None:
- try:
- text_family = TextFamily(oref, version=panel["currVersions"]["en"], lang="en", version2=panel["currVersions"]["he"], lang2="he", commentary=False,
- context=True, pad=True, alts=True, wrapLinks=False, translationLanguagePreference=kwargs.get("translationLanguagePreference", None)).contents()
- except NoVersionFoundError:
- text_family = {}
- text_family["updateFromAPI"] = True
- text_family["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None
- text_family["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None
- panel["text"] = text_family
+ primary_params = [primaryVersion['languageFamilyName'], primaryVersion['versionTitle']]
+ primary_params[0] = primary_params[0] or 'primary'
+ translation_params = [translationVersion['languageFamilyName'], translationVersion['versionTitle']]
+ translation_params[0] = translation_params[0] or 'translation'
+ text_adapter = TextRequestAdapter(oref.section_ref(), [primary_params, translation_params], return_format='wrap_all_entities')
+ text = text_adapter.get_versions_for_query()
+ # text['ref'] = oref.normal()
+ #now we we should add the he and text attributes
+ if len(text['versions']) == 2:
+ if text['versions'][0]['isPrimary'] and not text['versions'][1]['isSource']:
+ text['he'], text['text'] = text['versions'][0]['text'], text['versions'][1]['text']
+ else:
+ text['he'], text['text'] = text['versions'][1]['text'], text['versions'][0]['text']
+ elif len(text['versions']) == 1:
+ if primary_params == translation_params:
+ text['he'] = text['text'] = text['versions'][0]['text']
+ elif [text['versions'][0]['languageFamilyName'], text['versions'][0]['versionTitle']] == translation_params:
+ text['text'], text['he'] = text['versions'][0]['text'], []
+ else:
+ text['he'], text['text'] = text['versions'][0]['text'], []
+
+ text["updateFromAPI"] = True
+ panel["text"] = text
if oref.index.categories == ["Tanakh", "Torah"]:
panel["indexDetails"] = oref.index.contents() # Included for Torah Parashah titles rendered in text
@@ -460,7 +481,7 @@ def make_sheet_panel_dict(sheet_id, filter, **kwargs):
return panels
-def make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs):
+def make_panel_dicts(oref, primaryVersion, translationVersion, filter, versionFilter, multi_panel, **kwargs):
"""
Returns an array of panel dictionaries.
Depending on whether `multi_panel` is True, connections set in `filter` are displayed in either 1 or 2 panels.
@@ -468,16 +489,22 @@ def make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_pa
panels = []
# filter may have value [], meaning "all". Therefore we test filter with "is not None".
if filter is not None and multi_panel:
- panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)]
- panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Connections", **kwargs)]
+ panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Text", **kwargs)]
+ panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Connections", **kwargs)]
elif filter is not None and not multi_panel:
- panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "TextAndConnections", **kwargs)]
+ panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "TextAndConnections", **kwargs)]
else:
- panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)]
+ panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Text", **kwargs)]
return panels
+def _extract_version_params(request, key):
+ params = request.GET.get(key, '|')
+ params = params.replace("_", " ")
+ languageFamilyName, versionTitle = params.split('|')
+ return {'languageFamilyName': languageFamilyName, 'versionTitle': versionTitle}
+
@sanitize_get_params
def text_panels(request, ref, version=None, lang=None, sheet=None):
"""
@@ -495,12 +522,8 @@ def text_panels(request, ref, version=None, lang=None, sheet=None):
panels = []
multi_panel = not request.user_agent.is_mobile and not "mobile" in request.GET
# Handle first panel which has a different signature in params
- versionEn = request.GET.get("ven", None)
- if versionEn:
- versionEn = versionEn.replace("_", " ")
- versionHe = request.GET.get("vhe", None)
- if versionHe:
- versionHe = versionHe.replace("_", " ")
+ primaryVersion = _extract_version_params(request, 'vhe')
+ translationVersion = _extract_version_params(request, 'ven')
filter = request.GET.get("with").replace("_", " ").split("+") if request.GET.get("with") else None
filter = [] if filter == ["all"] else filter
@@ -510,11 +533,7 @@ def text_panels(request, ref, version=None, lang=None, sheet=None):
if sheet == None:
versionFilter = [request.GET.get("vside").replace("_", " ")] if request.GET.get("vside") else []
- if versionEn and not Version().load({"versionTitle": versionEn, "language": "en"}):
- raise Http404
- if versionHe and not Version().load({"versionTitle": versionHe, "language": "he"}):
- raise Http404
- versionEn, versionHe = override_version_with_preference(oref, request, versionEn, versionHe)
+ # versionEn, versionHe = override_version_with_preference(oref, request, versionEn, versionHe) #TODO
kwargs = {
"panelDisplayLanguage": request.GET.get("lang", request.contentLang),
@@ -534,7 +553,7 @@ def text_panels(request, ref, version=None, lang=None, sheet=None):
kwargs["sidebarSearchQuery"] = request.GET.get("sbsq", None)
kwargs["selectedNamedEntity"] = request.GET.get("namedEntity", None)
kwargs["selectedNamedEntityText"] = request.GET.get("namedEntityText", None)
- panels += make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs)
+ panels += make_panel_dicts(oref, primaryVersion, translationVersion, filter, versionFilter, multi_panel, **kwargs)
elif sheet == True:
panels += make_sheet_panel_dict(ref, filter, **{"panelDisplayLanguage": request.GET.get("lang",request.contentLang), "referer": request.path})
diff --git a/sefaria/datatype/jagged_array.py b/sefaria/datatype/jagged_array.py
index 5d35642afb..3957960061 100644
--- a/sefaria/datatype/jagged_array.py
+++ b/sefaria/datatype/jagged_array.py
@@ -659,12 +659,13 @@ def modify_by_function(self, func, start_sections=None, _cur=None, _curSections=
Func should accept two parameters: 1) text of current segment 2) zero-indexed indices of segment
:param start_sections: array(int), optional param. Sections passed to `func` will be offset by `start_sections`, if passed
"""
- _curSections = _curSections or []
if _cur is None:
_cur = self._store
if isinstance(_cur, str):
+ _curSections = _curSections or [0]
return func(_cur, self.get_offset_sections(_curSections, start_sections))
elif isinstance(_cur, list):
+ _curSections = _curSections or []
return [self.modify_by_function(func, start_sections, temp_curr, _curSections + [i]) for i, temp_curr in enumerate(_cur)]
def flatten_to_array(self, _cur=None):
diff --git a/sefaria/model/dependencies.py b/sefaria/model/dependencies.py
index eb842a16bb..2a186610f2 100644
--- a/sefaria/model/dependencies.py
+++ b/sefaria/model/dependencies.py
@@ -57,7 +57,7 @@ def process_version_title_change_in_search(ver, **kwargs):
text_index = library.get_index(ver.title)
delete_version(text_index, kwargs.get("old"), ver.language)
for ref in text_index.all_segment_refs():
- TextIndexer.index_ref(search_index_name, ref, kwargs.get("new"), ver.language, False)
+ TextIndexer.index_ref(search_index_name, ref, kwargs.get("new"), ver.language, ver.languageFamilyName, ver.isPrimary)
# Version Title Change
diff --git a/sefaria/model/text_reuqest_adapter.py b/sefaria/model/text_reuqest_adapter.py
index d98aa31ba1..3030522300 100644
--- a/sefaria/model/text_reuqest_adapter.py
+++ b/sefaria/model/text_reuqest_adapter.py
@@ -98,7 +98,6 @@ def _add_ref_data_to_return_obj(self) -> None:
'heSectionRef': oref.section_ref().he_normal(),
'firstAvailableSectionRef': oref.first_available_section_ref().normal(),
'isSpanning': oref.is_spanning(),
- 'spanningRefs': [r.normal() for r in oref.split_spanning_ref()],
'next': oref.next_section_ref().normal() if oref.next_section_ref() else None,
'prev': oref.prev_section_ref().normal() if oref.prev_section_ref() else None,
'title': oref.context_ref().normal(),
@@ -107,6 +106,8 @@ def _add_ref_data_to_return_obj(self) -> None:
'primary_category': oref.primary_category,
'type': oref.primary_category, #same as primary category
})
+ if self.return_obj['isSpanning']:
+ self.return_obj['spanningRefs'] = [r.normal() for r in oref.split_spanning_ref()]
def _add_index_data_to_return_obj(self) -> None:
index = self.oref.index
diff --git a/sefaria/search.py b/sefaria/search.py
index ef534422ed..f9728a8f57 100644
--- a/sefaria/search.py
+++ b/sefaria/search.py
@@ -541,7 +541,7 @@ def index_version(cls, version, tries=0, action=None):
print("Could not find dictionary node in {}".format(version.title))
@classmethod
- def index_ref(cls, index_name, oref, version_title, lang):
+ def index_ref(cls, index_name, oref, version_title, lang, language_family_name, is_primary):
# slower than `cls.index_version` but useful when you don't want the overhead of loading all versions into cache
cls.index_name = index_name
cls.curr_index = oref.index
@@ -558,7 +558,7 @@ def index_ref(cls, index_name, oref, version_title, lang):
content = TextChunk(oref, lang, vtitle=version_title).ja().flatten_to_string()
categories = cls.curr_index.categories
tref = oref.normal()
- doc = cls.make_text_index_document(tref, oref.he_normal(), version_title, lang, version_priority, content, categories, hebrew_version_title)
+ doc = cls.make_text_index_document(tref, oref.he_normal(), version_title, lang, version_priority, content, categories, hebrew_version_title, language_family_name, is_primary)
id = make_text_doc_id(tref, version_title, lang)
es_client.index(index_name, doc, id=id)
@@ -567,11 +567,13 @@ def _cache_action(cls, segment_str, tref, heTref, version):
# Index this document as a whole
vtitle = version.versionTitle
vlang = version.language
+ language_family_name = version.languageFamilyName
+ is_primary = version.isPrimary
hebrew_version_title = getattr(version, 'versionTitleInHebrew', None)
try:
version_priority, categories = cls.version_priority_map[(version.title, vtitle, vlang)]
#TODO include sgement_str in this func
- doc = cls.make_text_index_document(tref, heTref, vtitle, vlang, version_priority, segment_str, categories, hebrew_version_title)
+ doc = cls.make_text_index_document(tref, heTref, vtitle, vlang, version_priority, segment_str, categories, hebrew_version_title, language_family_name, is_primary)
# print doc
except Exception as e:
logger.error("Error making index document {} / {} / {} : {}".format(tref, vtitle, vlang, str(e)))
@@ -613,7 +615,7 @@ def modify_text_in_doc(cls, content):
return content
@classmethod
- def make_text_index_document(cls, tref, heTref, version, lang, version_priority, content, categories, hebrew_version_title):
+ def make_text_index_document(cls, tref, heTref, version, lang, version_priority, content, categories, hebrew_version_title, language_family_name, is_primary):
"""
Create a document for indexing from the text specified by ref/version/lang
"""
@@ -653,6 +655,8 @@ def make_text_index_document(cls, tref, heTref, version, lang, version_priority,
"exact": content,
"naive_lemmatizer": content,
'hebrew_version_title': hebrew_version_title,
+ "languageFamilyName": language_family_name,
+ "isPrimary": is_primary,
}
@@ -734,7 +738,7 @@ def index_from_queue():
queue = db.index_queue.find()
for item in queue:
try:
- TextIndexer.index_ref(index_name, Ref(item["ref"]), item["version"], item["lang"])
+ TextIndexer.index_ref(index_name, Ref(item["ref"]), item["version"], item["lang"], item['languageFamilyName'], item['isPrimary'])
db.index_queue.remove(item)
except Exception as e:
logging.error("Error indexing from queue ({} / {} / {}) : {}".format(item["ref"], item["version"], item["lang"], e))
diff --git a/sourcesheets/views.py b/sourcesheets/views.py
index 5f25f971fc..65c9de1c69 100644
--- a/sourcesheets/views.py
+++ b/sourcesheets/views.py
@@ -2,7 +2,7 @@
import json
import httplib2
from urllib3.exceptions import NewConnectionError
-from urllib.parse import unquote
+from urllib.parse import unquote, urlencode
from elasticsearch.exceptions import AuthorizationException
from datetime import datetime
from io import StringIO, BytesIO
@@ -202,6 +202,15 @@ def view_sheet(request, sheet_id, editorMode = False):
editor = request.GET.get('editor', '0')
embed = request.GET.get('embed', '0')
+ interface_lang = request.interfaceLang
+ content_lang = request.GET.get("lang", request.contentLang)
+ fixed_content_lang = 'he' if interface_lang == 'hebrew' else 'bi'
+ if content_lang != fixed_content_lang:
+ query_params = request.GET.dict()
+ query_params['lang'] = fixed_content_lang
+ new_url = f"/sheets/{sheet_id}?{urlencode(query_params)}"
+ return redirect(new_url, permanent=True)
+
if editor != '1' and embed !='1' and editorMode is False:
return catchall(request, sheet_id, True)
diff --git a/static/css/s2.css b/static/css/s2.css
index 1adb3a704f..525cc2daf1 100644
--- a/static/css/s2.css
+++ b/static/css/s2.css
@@ -1543,9 +1543,19 @@ div.interfaceLinks-row a {
.readerPanel.bilingual .readerNavMenu .gridBox {
direction: ltr;
}
-.readerPanel.english .he {
+.readerPanel.english .contentText .he,
+.readerPanel.hebrew .contentText .en,
+.readerPanel.english .contentSpan.primary,
+.readerPanel.english .languageToggle .he,
+.readerPanel.hebrew .contentSpan.translation,
+.readerPanel.hebrew .languageToggle .en {
display: none;
}
+.readerPanel.english .versionsTextList .primary,
+.readerPanel.hebrew .versionsTextList .translation {
+ display: block;
+}
+
.readerPanel.english .he.heOnly{
display: inline;
text-align: right;
@@ -1557,11 +1567,10 @@ div.interfaceLinks-row a {
display: inline;
text-align: right;
}
-.readerPanel.hebrew .en {
- display: none;
-}
.readerPanel.english .heOnly .he,
-.readerPanel.bilingual .heOnly .he {
+.readerPanel.bilingual .heOnly .he,
+.readerPanel.english .enOnly .en,
+.readerPanel.bilingual .enOnly .en {
display: inline;
}
.languageToggle {
@@ -4187,6 +4196,9 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item:hover{
body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus {
text-decoration: underline;
}
+.singlePanel .searchContent h1 {
+ height: revert;
+}
.searchContent h1 {
height: 40px;
font-size: 30px;
@@ -4930,9 +4942,19 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
display: flex;
justify-content: space-between;
padding: 20px 0;
+}
+
+.readerNavMenu .sheet:not(.profile-page .sheet) {
border-bottom: 1px solid #eee;
color: black;
}
+
+.sheetsProfileList .sheet {
+ display: flex;
+ justify-content: space-between;
+ padding: 20px 0;
+}
+
.readerNavMenu .sheet:hover{
text-decoration: none;
}
@@ -4987,6 +5009,10 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.tagsList .enOnly {
direction: ltr;
}
+.readerControlsOuter {
+ position: relative;
+ z-index: 103;
+}
.readerControls {
position: relative;
top: 0;
@@ -4994,7 +5020,6 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
width: 100%;
box-sizing: border-box;
text-align: center;
- z-index: 100;
height: 60px;
line-height: 60px;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
@@ -5023,13 +5048,14 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
background-color: #EDEDED;
}
.readerControls .connectionsPanelHeader .connectionsHeaderTitle {
- text-transform: uppercase;
letter-spacing: 1px;
font-size: 16px;
font-weight: lighter;
}
+.readerControls .connectionsPanelHeader .connectionsHeaderTitle:not(.active) {
+ text-transform: uppercase;
+}
.connectionsPanelHeader .connectionsHeaderTitle.active {
- text-transform: none;
cursor: pointer;
}
.connectionsHeaderTitle .fa-chevron-left {
@@ -5059,7 +5085,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
}
.readerControls.transLangPrefSuggBann {
background-color: #EDEDEC;
- z-index: 99;
+ z-index: 2;
}
.readerControls .readerControlsInner.transLangPrefSuggBannInner {
justify-content: center;
@@ -5149,7 +5175,6 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
}
.readerControls.connectionsHeader .readerTextToc {
font-family: "Roboto", "Helvetica Neue", "Helvetica", sans-serif;
- text-transform: uppercase;
color: #666;
width: 100%;
}
@@ -5181,6 +5206,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
display: flex;
flex-direction: row;
text-align: right;
+ align-items: center;
}
/* icons need a little nudge in flipped hebrew mode */
.interface-hebrew .rightButtons {
@@ -5598,7 +5624,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.bilingual .sheetContent .title .he {
display: none;
}
-.interface-hebrew .readerPanel.english .textRange,
+.interface-hebrew .readerPanel.ltr .textRange,
.interface-hebrew .readerPanel.english .categoryFilterGroup,
.interface-hebrew .readerPanel.bilingual .categoryFilterGroup,
.interface-hebrew .readerPanel.english .essayGroup,
@@ -5607,6 +5633,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.interface-hebrew .readerPanel.bilingual .textTableOfContents {
direction: ltr;
}
+.interface-english .readerPanel.rtl .textRange,
.interface-english .readerPanel.hebrew .textRange,
.interface-english .readerPanel.hebrew .categoryFilterGroup,
.interface-english .readerPanel.hebrew .essayGroup,
@@ -5715,7 +5742,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
display: inline;
}*/
.bilingual .segment > .he,
-.bilingual .segment > p > .he{
+.bilingual .segment > p > .he {
display: block;
}
.bilingual .segment > .en,
@@ -5743,11 +5770,14 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.stacked.bilingual .sheetContent .segment > p > .en {
margin-top: 0;
}
-.stacked.bilingual .basetext .segment > .en ,
-.stacked.bilingual .basetext .segment > p > .en {
+.stacked.bilingual .basetext .segment > .translation ,
+.stacked.bilingual .basetext .segment > p > .translation {
margin: 10px 0 20px;
color: #666;
}
+.stacked.bilingual .basetext .segment > p > .he.translation {
+ color: black;
+}
.stacked.bilingual .segment.heOnly > .he,
.stacked.bilingual .segment.enOnly > .en,
.stacked.bilingual .segment.heOnly > p > .he,
@@ -5802,20 +5832,28 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.readerPanel.english .SheetSource .sheetItem.segment .en {
background-color: white;
}
-.heLeft.bilingual .segment > .en,
-.heRight.bilingual .segment > .he ,
-.heLeft.bilingual .segment > p > .en,
-.heRight.bilingual .segment > p > .he {
+.heLeft.bilingual .segment > .translation,
+.heRight.bilingual .segment > .primary,
+.heLeft.bilingual .segment > p > .translation,
+.heRight.bilingual .segment > p > .primary,
+.heRight.bilingual .sheetItem.segment > .he,
+.heLeft.bilingual .sheetItem.segment > .en{
float: right;
padding-left: 20px;
}
-.heRight.bilingual .segment > .en,
-.heLeft.bilingual .segment > .he,
-.heRight.bilingual .segment > p > .en,
-.heLeft.bilingual .segment > p > .he {
+.heRight.bilingual .segment > .translation,
+.heLeft.bilingual .segment > .primary,
+.heRight.bilingual .segment > p > .translation,
+.heLeft.bilingual .segment > p > .primary,
+.heRight.bilingual .sheetItem.segment > .en,
+.heLeft.bilingual .sheetItem.segment > .he{
float: left;
padding-right: 20px;
}
+.segment > p > .he.translation {
+ --hebrew-font: var(--hebrew-sans-serif-font-family);
+ font-size: 100%;
+}
.basetext .segment:active,
.basetext .segment:focus {
background-color: #f5faff;
@@ -5904,6 +5942,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus
.segment.heOnly .en{
display: none;
}
+/*in the text reader we don't have enOnly anymore. it always hvae primary (which is one the meaning of heOnly) maybe this is useful for other cases*/
.segment.enOnly .he{
display: none;
}
@@ -7987,7 +8026,9 @@ But not to use a display block directive that might break continuous mode for ot
--hebrew-font: var(--hebrew-sans-serif-font-family);
border: none;
}
-
+.saveProfileButton{
+ background-color: var(--midrash-green);
+}
.button.disabled{
border: 1px solid var(--light-grey);
background-color: var(--lightest-grey);
@@ -8421,9 +8462,19 @@ a .button:hover {
display: flex;
flex-direction: row;
justify-content: space-between;
+
}
.profile-page .collectionListing {
padding: 20px 0;
+ margin-top: 20px;
+ background-color: white;
+ border: 1000px solid white;
+ border-top: 4px solid white;
+ border-bottom: 120px solid white;
+ margin: -4px 0 -120px 0;
+ margin-inline-start: -1000px;
+ margin-inline-end: 0;
+ width: 100%;
}
.collectionListing .left-content {
display: flex;
@@ -8787,6 +8838,9 @@ body.interface-english .publishBox .react-tags__suggestions ul {
.collectionListingDetails {
color: #999;
font-size: 14px;
+ font-family: Roboto !important;
+ font-weight: 400;
+ line-height: 18px;
}
.collectionListingMembership {
text-transform: capitalize;
@@ -8896,7 +8950,10 @@ body.interface-english .publishBox .react-tags__suggestions ul {
flex: 1;
}
.sheet .sheetTitleText {
- font-family: var(--english-serif-font-family);
+ font-family: Roboto;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 18.75px;
}
.sheetListingPinButton {
display: none;
@@ -10834,11 +10891,14 @@ section.SheetOutsideBiText {
.readerPanel.hebrew section.SheetSource .sheetItem > .he,
.readerPanel.english section.SheetSource .sheetItem > .en,
+.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .en > .sourceContentText,
.readerPanel.hebrew section.SheetOutsideBiText .sheetItem > .he,
.readerPanel.english section.SheetOutsideBiText .sheetItem > .en {
display: block;
}
-.readerPanel.hebrew section.SheetSource .sheetItem > .en,
+.readerPanel.hebrew section.SheetSource .sheetItem:not(.enOnly) > .en,
+.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .en > .ref,
+.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .he > .sourceContentText,
.readerPanel.english section.SheetSource .sheetItem > .he,
.readerPanel.hebrew section.SheetOutsideBiText .sheetItem > .en,
.readerPanel.english section.SheetOutsideBiText .sheetItem > .he {
@@ -11512,8 +11572,8 @@ cursor: pointer;
.sheetList .sheet .sheetTags {
color: #999;
}
-.sheetList .sheet .sheetTags .bullet {
- margin: 0 5px;
+.sheetsProfileList .sheet .sheetTags .bullet {
+ padding: 0 5px;
}
.sheetList .sheet a {
color: inherit;
@@ -11649,8 +11709,12 @@ cursor: pointer;
top: -2px;
}
.profile-page .collectionListingName {
- font-size: 18px;
margin-bottom: 10px;
+ font-family: Roboto;
+ font-size: 16px;
+ font-weight: 600;
+ line-height: 18.75px;
+
}
.profile-page .collectionListing + .collectionListing {
border-top: 0;
@@ -11699,6 +11763,10 @@ cursor: pointer;
margin-top: 20px;
font-size: 18px;
line-height: 1.4;
+ font-family: Roboto;
+ font-size: 14px;
+ font-weight: 400;
+ line-height: 16.41px;
}
@media (max-width: 540px) {
.profile-page .profile-pic:hover .profile-pic-hover-button.profile-pic-button-visible {
@@ -11721,7 +11789,9 @@ cursor: pointer;
font-size: 43.5px !important;
}
.profile-page .profile-summary .profile-actions {
+ display: flex;
flex-wrap: wrap;
+ flex-direction: row;
}
}
.profile-page .profile-summary .follow {
@@ -11732,11 +11802,7 @@ cursor: pointer;
.profile-page .profile-summary .follow .follow-bull {
padding: 0 5px;
}
-.profile-page .profile-summary .profile-actions {
- display: flex;
- flex-direction: row;
- margin: 10px 0;
-}
+
.profile-page .resourcesLink.faded {
color: #666;
}
@@ -13580,6 +13646,224 @@ span.ref-link-color-3 {color: blue}
cursor: pointer;
}
+.readerDropdownMenu {
+ position: relative;
+ display: flex;
+ flex-direction: row-reverse;
+ z-index: 3;
+}
+
+.dropdownLinks-menu {
+ display: contents;
+}
+
+.dropdownLinks-menu.closed {
+ display: none;
+}
+
+.texts-properties-menu {
+ width: 256px;
+ border: 1px solid var(--lighter-grey);
+ border-radius: 5px;
+ box-shadow: 0px 2px 4px var(--lighter-grey);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 100%;
+ background-color: white;
+ --english-font: var(--english-sans-serif-font-family);
+ --hebrew-font: var(--hebrew-sans-serif-font-family);
+}
+
+.dropdownLinks-button {
+ border: none;
+ background-color: inherit;
+}
+
+.rightButtons .dropdownLinks-button {
+ text-align: end;
+}
+
+.toggle-switch-container {
+ align-items: center;
+ display: flex;
+ direction: ltr;
+}
+
+.toggle-switch {
+ position: relative;
+ width: 46px;
+ display: inline-block;
+ text-align: left;
+}
+
+.toggle-switch-checkbox {
+ display: none;
+}
+
+.toggle-switch-label {
+ display: block;
+ overflow: hidden;
+ cursor: pointer;
+ border: 0 solid var(--light-grey);
+ border-radius: 20px;
+}
+
+.toggle-switch-inner {
+ display: block;
+ width: 200%;
+ margin-left: -100%;
+ transition: margin 0.3s ease-in 0s;
+}
+
+.toggle-switch-inner:before,
+.toggle-switch-inner:after {
+ float: left;
+ width: 50%;
+ height: 24px;
+ padding: 0;
+ line-height: 24px;
+ color: white;
+ font-weight: bold;
+ box-sizing: border-box;
+ content: "";
+ color: white;
+}
+
+.toggle-switch-inner:before {
+ padding-left: 10px;
+ background-color: var(--sefaria-blue);
+}
+
+.toggle-switch-inner:after {
+ padding-right: 10px;
+ background-color: var(--light-grey);
+}
+
+.toggle-switch-switch {
+ display: block;
+ width: 20px;
+ height: 20px;
+ background: white;
+ position: absolute;
+ top: 50%;
+ bottom: 0;
+ right: 24px;
+ border: 0 solid var(--light-grey);
+ border-radius: 20px;
+ transition: all 0.3s ease-in 0s;
+ transform: translateY(-50%);
+}
+
+.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner {
+ margin-left: 0;
+}
+
+.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-switch {
+ right: 2px;
+}
+
+.toggle-switch-checkbox:disabled + .toggle-switch-label .toggle-switch-inner:after {
+ background-color: var(--lighter-grey);
+}
+
+.toggle-switch-line {
+ display: flex;
+ width: 216px;
+ height: 49px;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.toggle-switch-line:is(.disabled) .int-en,
+.toggle-switch-line:is(.disabled) .int-he {
+ color: var(--light-grey);
+}
+
+.texts-properties-menu .int-en,
+.texts-properties-menu .int-he {
+ align-content: center;
+}
+
+.show-source-translation-buttons {
+ display: flex;
+ flex-direction: column;
+ height: 170px;
+ justify-content: center;
+}
+
+.show-source-translation-buttons .button {
+ margin: unset;
+ display: flex;
+ height: 46px;
+ width: 235px;
+ align-items: center;
+ justify-content: center;
+ margin: 3px 0;
+}
+
+.show-source-translation-buttons .button:not(.checked) {
+ background-color: var(--lighter-grey);
+ color: black;
+}
+
+.layout-button-line {
+ height: 57px;
+ width: 216px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.layout-options {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 16px;
+}
+
+.layout-button {
+ border: none;
+ width: 28px;
+ height: 24px;
+ -webkit-mask: var(--url) no-repeat;
+ -webkit-mask-size: contain;
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ -webkit-mask-size: 100% 100%;
+ background-color: var(--medium-grey);
+ cursor: pointer;
+}
+
+.layout-button.checked {
+ background-color: var(--sefaria-blue);
+}
+
+.text-menu-border {
+ width: 100%;
+ height: 1px;
+ background-color: var(--lighter-grey);
+}
+
+.font-size-line {
+ width: 230px;
+ height: 50px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ direction: ltr;
+}
+
+.font-size-button {
+ display: flex;
+ align-items: center;
+ background-color: white;
+ border: none;
+ cursor: pointer;
+}
+
#currentlyReadingContainer {
margin: 5px 30px;
flex-grow: 1;
@@ -13758,7 +14042,7 @@ span.ref-link-color-3 {color: blue}
.dropdownSeparator {
border: 1px solid var(--light-grey);
}
-.header .dropdownLinks {
+.header .headerDropdownMenu {
display: block;
align-items: center;
margin-top: 2px;
@@ -13770,21 +14054,21 @@ span.ref-link-color-3 {color: blue}
-moz-margin-start: 20px;
margin-inline-start: 10px;
}
-.header .dropdownLinks img {
+.header .headerDropdownMenu img {
height: 18px;
width: 18px;
vertical-align: middle;
}
-.interface-english .header .dropdownLinks img {
+.interface-english .header .headerDropdownMenu img {
margin-inline-end: 2px;
}
-.interface-hebrew .header .dropdownLinks img {
+.interface-hebrew .header .headerDropdownMenu img {
margin-inline-end: 6px;
}
-/* .header .dropdownLinks a.dropdownLinks-button::after {
+/* .header .headerDropdownMenu a.dropdownLinks-button::after {
display: inline-block;
background: no-repeat url("/static/icons/chevron-down.svg");
height: 10px;
@@ -13793,7 +14077,7 @@ span.ref-link-color-3 {color: blue}
content: "";
transform: scale(0.75);
} */
-.header .dropdownLinks .dropdownLinks-menu {
+.header .headerDropdownMenu .dropdownLinks-menu {
top: 35px;
position: absolute;
z-index: 1;
@@ -13804,12 +14088,12 @@ span.ref-link-color-3 {color: blue}
display:none;
overflow:hidden;
}
-.interface-english .header .dropdownLinks .dropdownLinks-menu {
+.interface-english .header .headerDropdownMenu .dropdownLinks-menu {
right: 0px;
min-width: 150px;
width: max-content;
}
-.interface-hebrew .header .dropdownLinks .dropdownLinks-menu {
+.interface-hebrew .header .headerDropdownMenu .dropdownLinks-menu {
left: 0px;
min-width: 150px;
width: max-content;
@@ -13820,7 +14104,7 @@ span.ref-link-color-3 {color: blue}
padding: 4px;
max-width: 220px;
}
-.header .dropdownLinks .dropdownLinks-menu.open {
+.header .headerDropdownMenu .dropdownLinks-menu.open {
display: block;
}
@@ -14113,6 +14397,57 @@ span.ref-link-color-3 {color: blue}
padding: 0px 4px;
}
+.profilePicAndButtonContainer {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+.createButtons {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-evenly;
+ padding-top: 20px;
+}
+
+.sheetsProfileList {
+ margin-top: 20px;
+ background-color: white;
+ border: 1000px solid white;
+ border-top: 4px solid white;
+ border-bottom: 120px solid white;
+ margin: -4px 0 -120px -1000px;
+ width: 100%;
+
+}
+
+.profile-page .sheetsProfileList {
+ margin: -4px 0 -120px 0;
+ margin-inline-start: -1000px;
+ margin-inline-end: 0;
+}
+
+.sheetsProfileList .readerNavMenuSearchButton {
+ margin: 0 10px 0 5px;
+ display: inline-flex;
+ top: 0;
+}
+.sheetsProfileList .readerNavMenuSearchButton {
+ margin-inline-start: 10px;
+ margin-inline-end: 5px;
+}
+.sheetsProfileList input {
+ border: 0;
+ font-size: 18px;
+ font-family: "adobe-garamond-pro", "Crimson Text", Georgia, serif;
+ background-color: transparent;
+}
+.sheetsProfileList .loadingMessage {
+ margin-top: 30px;
+}
+
@-webkit-keyframes load5 {
0%,100%{box-shadow:0 -2.6em 0 0 #ffffff,1.8em -1.8em 0 0 rgba(0,0,0,0.2),2.5em 0 0 0 rgba(0,0,0,0.2),1.75em 1.75em 0 0 rgba(0,0,0,0.2),0 2.5em 0 0 rgba(0,0,0,0.2),-1.8em 1.8em 0 0 rgba(0,0,0,0.2),-2.6em 0 0 0 rgba(0,0,0,0.5),-1.8em -1.8em 0 0 rgba(0,0,0,0.7)}
12.5%{box-shadow:0 -2.6em 0 0 rgba(0,0,0,0.7),1.8em -1.8em 0 0 #ffffff,2.5em 0 0 0 rgba(0,0,0,0.2),1.75em 1.75em 0 0 rgba(0,0,0,0.2),0 2.5em 0 0 rgba(0,0,0,0.2),-1.8em 1.8em 0 0 rgba(0,0,0,0.2),-2.6em 0 0 0 rgba(0,0,0,0.2),-1.8em -1.8em 0 0 rgba(0,0,0,0.5)}
diff --git a/static/css/static.css b/static/css/static.css
index 1459cd84f3..d5cc7756de 100644
--- a/static/css/static.css
+++ b/static/css/static.css
@@ -858,11 +858,6 @@ p.registration-links a:hover{
#editProfilePage #sheetsBox,
#editProfilePage #aboutBox {
margin-bottom: 60px;
- display: flex;
- display: -webkit-flex;
- justify-content: space-between;
- -webkit-justify-content:space-between;
- flex-flow: row wrap;
}
#editProfilePage #sheetsBox{
flex-direction: column;
@@ -906,7 +901,6 @@ p.registration-links a:hover{
}
#editProfilePage .field {
margin-bottom: 20px;
- width: 48%;
min-width: 300px;
}
#editProfilePage .field input {
diff --git a/static/icons/bi-ltr-heLeft.svg b/static/icons/bi-ltr-heLeft.svg
new file mode 100644
index 0000000000..2d28ee586f
--- /dev/null
+++ b/static/icons/bi-ltr-heLeft.svg
@@ -0,0 +1,50 @@
+
diff --git a/static/icons/bi-ltr-stacked.svg b/static/icons/bi-ltr-stacked.svg
new file mode 100644
index 0000000000..27fb13f535
--- /dev/null
+++ b/static/icons/bi-ltr-stacked.svg
@@ -0,0 +1,26 @@
+
diff --git a/static/icons/bi-rtl-heRight.svg b/static/icons/bi-rtl-heRight.svg
new file mode 100644
index 0000000000..67a7c095d0
--- /dev/null
+++ b/static/icons/bi-rtl-heRight.svg
@@ -0,0 +1,50 @@
+
diff --git a/static/icons/bi-rtl-stacked.svg b/static/icons/bi-rtl-stacked.svg
new file mode 100644
index 0000000000..035734e83f
--- /dev/null
+++ b/static/icons/bi-rtl-stacked.svg
@@ -0,0 +1,26 @@
+
diff --git a/static/icons/enlarge_font.svg b/static/icons/enlarge_font.svg
new file mode 100644
index 0000000000..3c1ff999d6
--- /dev/null
+++ b/static/icons/enlarge_font.svg
@@ -0,0 +1,3 @@
+
diff --git a/static/icons/mixed-beside-ltrrtl.svg b/static/icons/mixed-beside-ltrrtl.svg
new file mode 100644
index 0000000000..f19de475c1
--- /dev/null
+++ b/static/icons/mixed-beside-ltrrtl.svg
@@ -0,0 +1,50 @@
+
diff --git a/static/icons/mixed-beside-rtlltr.svg b/static/icons/mixed-beside-rtlltr.svg
new file mode 100644
index 0000000000..a35ebe96f5
--- /dev/null
+++ b/static/icons/mixed-beside-rtlltr.svg
@@ -0,0 +1,50 @@
+
diff --git a/static/icons/mixed-stacked-ltrrtl.svg b/static/icons/mixed-stacked-ltrrtl.svg
new file mode 100644
index 0000000000..a0d8d6bb75
--- /dev/null
+++ b/static/icons/mixed-stacked-ltrrtl.svg
@@ -0,0 +1,26 @@
+
diff --git a/static/icons/mixed-stacked-rtlltr.svg b/static/icons/mixed-stacked-rtlltr.svg
new file mode 100644
index 0000000000..3e088068f8
--- /dev/null
+++ b/static/icons/mixed-stacked-rtlltr.svg
@@ -0,0 +1,26 @@
+
diff --git a/static/icons/mono-continuous.svg b/static/icons/mono-continuous.svg
new file mode 100644
index 0000000000..f84c2f3fa8
--- /dev/null
+++ b/static/icons/mono-continuous.svg
@@ -0,0 +1,26 @@
+
diff --git a/static/icons/mono-segmented.svg b/static/icons/mono-segmented.svg
new file mode 100644
index 0000000000..9f1b6bffa9
--- /dev/null
+++ b/static/icons/mono-segmented.svg
@@ -0,0 +1,5 @@
+
diff --git a/static/icons/reduce_font.svg b/static/icons/reduce_font.svg
new file mode 100644
index 0000000000..0714abe7fb
--- /dev/null
+++ b/static/icons/reduce_font.svg
@@ -0,0 +1,3 @@
+
diff --git a/static/js/AboutBox.jsx b/static/js/AboutBox.jsx
index 1ede90b49b..a0e2040944 100644
--- a/static/js/AboutBox.jsx
+++ b/static/js/AboutBox.jsx
@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import Sefaria from './sefaria/sefaria';
import VersionBlock, {VersionsBlocksList} from './VersionBlock/VersionBlock';
import Component from 'react-class';
-import {InterfaceText} from "./Misc";
+import {InterfaceText, LoadingMessage} from "./Misc";
import {ContentText} from "./ContentText";
+import {VersionsTextList} from "./VersionsTextList";
import { SidebarModules } from './NavSidebar';
@@ -63,8 +64,8 @@ class AboutBox extends Component {
this.setState({versionLangMap: versionsByLang, currentVersionsByActualLangs:currentVersionsByActualLangs});
}
openVersionInSidebar(versionTitle, versionLanguage) {
- this.props.setConnectionsMode("Translation Open", {previousMode: "About"});
- this.props.setFilter(Sefaria.getTranslateVersionsKey(versionTitle, versionLanguage));
+ this.props.setConnectionsMode("Version Open", {previousMode: "About"});
+ this.props.setFilter(Sefaria.getTranslateVersionsKey(versionTitle, versionLanguage), 'About');
}
isSheet(){
return this.props.srefs[0].startsWith("Sheet");
@@ -94,14 +95,36 @@ class AboutBox extends Component {
return ;
}
+ if (!Object.keys(this.state.versionLangMap).length) {
+ return (
+
+
+
+ );
+ }
+
+ if (this.props.mode === "Version Open") {
+ return (
+
+ );
+ }
+
const category = Sefaria.index(this.state?.details?.title)?.primary_category;
const isDictionary = d?.lexiconName;
- const sourceVersion = this.state.currentVersionsByActualLangs?.he;
- const translationVersions = Object.entries(this.state.currentVersionsByActualLangs).filter(([lang, version]) => lang != "he").map(([lang, version])=> version);
- const multiple_translations = translationVersions?.length > 1;
- const no_source_versions = multiple_translations || translationVersions?.length == 1 && !sourceVersion;
+ const sourceVersion = this.props.currObjectVersions.he;
+ const translationVersion = this.props.currObjectVersions?.en;
+ const no_source_versions = !sourceVersion;
const sourceVersionSectionTitle = {en: "Current Version", he:"מהדורה נוכחית"};
- const translationVersionsSectionTitle = multiple_translations ? {en: "Current Translations", he:"תרגומים נוכחיים"} : {en: "Current Translation", he:"תרגום נוכחי"};
+ const translationVersionSectionTitle = {en: "Current Translation", he:"תרגום נוכחי"};
const alternateVersionsSectionTitle = no_source_versions ? {en: "Source Versions", he:"מהדורות בשפת המקור"} : {en: "Alternate Source Versions", he:"מהדורות נוספות בשפת המקור"}
let detailSection = null;
@@ -163,8 +186,10 @@ class AboutBox extends Component {
{ !!placeTextEn || !!dateTextEn ?
- {`Composed: ${!!placeTextEn ? placeTextEn : ""} ${!!dateTextEn ? dateTextEn : ""}`}
- {`נוצר/נערך: ${!!placeTextHe ? placeTextHe : ""} ${!!dateTextHe ? dateTextHe : ""}`}
+
: null
}
@@ -188,25 +213,21 @@ class AboutBox extends Component {
: null );
const versionSectionEn =
- (!!translationVersions?.length ?
+ (!!translationVersion?.versionTitle ?
-
+
- {
- translationVersions.map((ve) => (
-
- ))
- }
+
: null );
const alternateSectionHe =
(Object.values(this.state.versionLangMap).some(array => array?.length) ?
@@ -243,6 +264,9 @@ AboutBox.propTypes = {
masterPanelLanguage: PropTypes.oneOf(["english", "hebrew", "bilingual"]),
title: PropTypes.string.isRequired,
srefs: PropTypes.array.isRequired,
+ vFilter: PropTypes.array,
+ onRangeClick: PropTypes.func,
+ onCitationClick: PropTypes.func,
};
diff --git a/static/js/AddToSourceSheet.jsx b/static/js/AddToSourceSheet.jsx
index 3b456e1a11..a2cb3f3f5c 100644
--- a/static/js/AddToSourceSheet.jsx
+++ b/static/js/AddToSourceSheet.jsx
@@ -117,71 +117,103 @@ class AddToSourceSheetBox extends Component {
normalize(text){
return(text.replaceAll(/(
)+/g, ' ').replace(/\u2009/g, ' ').replace(/<[^>]*>/g, ''));
}
- async addToSourceSheet() {
- if (!Sefaria._uid) {
- this.props.toggleSignUpModal(SignUpModalKind.AddToSheet);
- }
- if (!this.state.selectedSheet || !this.state.selectedSheet.id) { return; }
+
+ async postToSheet(source) {
+ if (this.checkContentForImages(source.refs)) {
const url = "/api/sheets/" + this.state.selectedSheet.id + "/add";
- const language = this.props.contentLanguage;
- let source = {};
- if(this.props.en || this.props.he){ // legacy code to support a call to this component in Gardens.
- if(this.props.srefs){ //we are saving a ref + ref's text, generally all fields should be present.
- source.refs = this.props.srefs;
- source.en = this.props.en;
- source.he = this.props.he;
- }else{ // an outside free text is being passed in. theoretically supports any interface that passes this in. In practice only legacy Gardens code.
- if (this.props.en && this.props.he) {
- source.outsideBiText = {he: this.props.he, en: this.props.en};
- } else {
- source.outsideText = this.props.en || this.props.he;
- }
- }
- } else if (this.props.srefs) { //regular use - this is currently the case when the component is loaded in the sidepanel or in the modal component via profiles and notes pages
- source.refs = this.props.srefs;
+ let postData = {source: JSON.stringify(source)};
+ if (this.props.note) {
+ postData.note = this.props.note;
+ }
+ await $.post(url, postData, this.confirmAdd);
+ }
+ }
+ makeSourceForEden() {
+ if (this.props.srefs) { //we are saving a ref + ref's text, generally all fields should be present.
+ source.refs = this.props.srefs;
+ source.en = this.props.en;
+ source.he = this.props.he;
+ } else { // an outside free text is being passed in. theoretically supports any interface that passes this in. In practice only legacy Gardens code.
+ if (this.props.en && this.props.he) {
+ source.outsideBiText = {he: this.props.he, en: this.props.en};
+ } else {
+ source.outsideText = this.props.en || this.props.he;
+ }
+ }
+ }
+ async handleSelectedWords(source, lan) {
+ // If something is highlighted and main panel language is not bilingual:
+ // Use passed in language to determine which version this highlight covers.
+ let selectedWords = this.props.selectedWords; //if there was highlighted single panel
+ const language = this.props.contentLanguage;
+ if (!selectedWords || language === "bilingual") {
+ return;
+ }
+ let segments = await sheetsUtils.getSegmentObjs(source.refs);
+ selectedWords = this.normalize(selectedWords);
+ segments = segments.map(segment => ({
+ ...segment,
+ [lan]: this.normalize(segment[lan])
+ }));
+ for (let iSegment = 0; iSegment < segments.length; iSegment++) {
+ const segment = segments[iSegment];
+ if (iSegment === 0){
+ let criticalIndex = this.longestSuffixPrefixIndex(segment[lan], selectedWords);
+ const ellipse = criticalIndex === 0 ? "" : "...";
+ segment[lan] = ellipse + segment[lan].slice(criticalIndex);
+ }
+ else if (iSegment == segments.length-1){
+ let criticalIndex = this.longestPrefixSuffixIndex(segment[lan], selectedWords);
+ const ellipse = criticalIndex === segment[lan].length-1 ? "" : "...";
+ const chunk = segment[lan].slice(0, criticalIndex)
+ segment[lan] = chunk + ellipse;
+ }
+ }
+ source[lan] = sheetsUtils.segmentsToSourceText(segments, lan);
+ }
+
+ async handleSameDirectionVersions() {
+ for (const lang of ['he', 'en']) {
+ const version = this.props.currObjectVersions[lang];
+ const source = {
+ refs: this.props.srefs,
+ [`version-${version.language}`]: version.versionTitle
+ }
+ await this.postToSheet(source);
+ }
+ }
- const { en, he } = this.props.currVersions ? this.props.currVersions : {"en": null, "he": null}; //the text we are adding may be non-default version
- if (he) { source["version-he"] = he; }
- if (en) { source["version-en"] = en; }
+ async addToSourceSheet() {
+ if (!Sefaria._uid) {
+ this.props.toggleSignUpModal(SignUpModalKind.AddToSheet);
+ }
+ if (!this.state.selectedSheet || !this.state.selectedSheet.id) {
+ return;
+ }
- // If something is highlighted and main panel language is not bilingual:
- // Use passed in language to determine which version this highlight covers.
- let selectedWords = this.props.selectedWords; //if there was highlighted single panel
- if (selectedWords && language != "bilingual") {
- let lan = language.slice(0,2);
- let segments = await sheetsUtils.getSegmentObjs(source.refs);
- selectedWords = this.normalize(selectedWords);
- segments = segments.map(segment => ({
- ...segment,
- [lan]: this.normalize(segment[lan])
- }));
- for (let iSegment = 0; iSegment < segments.length; iSegment++) {
- const segment = segments[iSegment];
- if (iSegment == 0){
- let criticalIndex = this.longestSuffixPrefixIndex(segment[lan], selectedWords);
- const ellipse = criticalIndex == 0 ? "" : "...";
- segment[lan] = ellipse + segment[lan].slice(criticalIndex);
- }
- else if (iSegment == segments.length-1){
- let criticalIndex = this.longestPrefixSuffixIndex(segment[lan], selectedWords);
- const ellipse = criticalIndex == segment[lan].length-1 ? "" : "...";
- const chunk = segment[lan].slice(0, criticalIndex)
- segment[lan] = chunk + ellipse;
- }
- }
+ const source = {};
+ let en, he;
+ if (this.props.en || this.props.he) { // legacy code to support a call to this component in Gardens.
+ this.makeSourceForEden();
+ } else if (this.props.srefs) { //regular use - this is currently the case when the component is loaded in the sidepanel or in the modal component via profiles and notes pages
+ source.refs = this.props.srefs;
- source[lan] = sheetsUtils.segmentsToSourceText(segments, lan);
- }
- }
- if (this.checkContentForImages(source.refs)) {
- let postData = {source: JSON.stringify(source)};
- if (this.props.note) {
- postData.note = this.props.note;
- }
- $.post(url, postData, this.confirmAdd);
+ ({ en, he } = this.props.currObjectVersions || {"en": null, "he": null}); //the text we are adding may be non-default version
+ if (en?.direction && en?.direction === he?.direction) {
+ await this.handleSameDirectionVersions();
+ return;
+ } else if (en?.direction === 'rtl' || he?.direction === 'ltr') {
+ ([en, he] = [he, en]);
}
+
+ if (he) { source["version-he"] = he.versionTitle; }
+ if (en) { source["version-en"] = en.versionTitle; }
+ }
+ const contentLang = he?.language || en?.language; // this matters only if one language is shown.
+ await this.handleSelectedWords(source, contentLang);
+ await this.postToSheet(source);
}
checkContentForImages(refs) {
// validate texts corresponding to refs have no images before posting them to sheet
diff --git a/static/js/BookPage.jsx b/static/js/BookPage.jsx
index 3469dbd436..e7514e0851 100644
--- a/static/js/BookPage.jsx
+++ b/static/js/BookPage.jsx
@@ -26,10 +26,12 @@ import ExtendedNotes from './ExtendedNotes';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Component from 'react-class';
-import {ContentLanguageContext} from './context';
+import {ReaderPanelContext} from './context';
import Hebrew from './sefaria/hebrew.js';
import ReactTags from 'react-tag-autocomplete';
+import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu";
+import {DropdownMenu} from "./common/DropdownMenu";
import Cookies from "js-cookie";
@@ -67,7 +69,7 @@ class BookPage extends Component {
}
getData() {
// Gets data about this text from cache, which may be null.
- return Sefaria.text(this.getDataRef(), {context: 1, enVersion: this.props.currVersions.en, heVersion: this.props.currVersions.he});
+ return Sefaria.text(this.getDataRef(), {context: 1, enVersion: this.props.currVersions.en?.versionTitle, heVersion: this.props.currVersions.he?.versionTitle});
}
loadData() {
// Ensures data this text is in cache, rerenders after data load if needed
@@ -89,7 +91,7 @@ class BookPage extends Component {
let currObjectVersions = {en: null, he: null};
for(let [lang,ver] of Object.entries(this.props.currVersions)){
if(!!ver){
- let fullVer = versions.find(version => version.versionTitle == ver && version.language == lang);
+ let fullVer = versions.find(version => version.versionTitle == ver.versionTitle && version.language == lang);
currObjectVersions[lang] = fullVer ? fullVer : null;
}
}
@@ -131,18 +133,15 @@ class BookPage extends Component {
currentVersion.merged = !!(currentVersion.sources);
return currentVersion;
}
- openVersion(version, language) {
+ openVersion(version, language, versionLanguageFamily) {
// Selects a version and closes this menu to show it.
// Calling this functon wihtout parameters resets to default
- this.props.selectVersion(version, language);
+ this.props.selectVersion(version, language, versionLanguageFamily);
this.props.close();
}
isBookToc() {
return (this.props.mode == "book toc")
}
- isTextToc() {
- return (this.props.mode == "text toc")
- }
extendedNotesBack(event){
return null;
}
@@ -168,7 +167,7 @@ class BookPage extends Component {
catUrl = "/texts/" + category;
}
- const readButton = !this.state.indexDetails || this.isTextToc() || this.props.compare ? null :
+ const readButton = !this.state.indexDetails || this.props.compare ? null :
Sefaria.lastPlaceForText(title) ?
Continue Reading
@@ -209,28 +208,21 @@ class BookPage extends Component {
return (
- {this.isTextToc() || this.props.compare ?
+ {this.props.compare ?
<>
- {this.props.compare ?
- : }
- {this.props.compare ?
{title}
- :
-
- Table of Contents
-
}
{Sefaria.interfaceLang !== "hebrew" ?
-
+ )}>
: }
diff --git a/static/js/ComparePanelHeader.jsx b/static/js/ComparePanelHeader.jsx
index 0de8a1ed3c..9939d8de2c 100644
--- a/static/js/ComparePanelHeader.jsx
+++ b/static/js/ComparePanelHeader.jsx
@@ -9,8 +9,11 @@ import {
SearchButton,
} from './Misc';
import {ContentText} from "./ContentText";
+import {DropdownMenu} from "./common/DropdownMenu";
+import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu";
+import {ReaderPanelContext} from "./context";
-const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, catTitle, heCatTitle,
+const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, catTitle, heCatTitle,
onBack, openSearch
}) => {
if (search) {
@@ -34,7 +37,7 @@ const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, ca
{Sefaria.interfaceLang !== "hebrew" ?
-
+ )}>
: null}
);
@@ -47,9 +50,9 @@ const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, ca
- {(Sefaria.interfaceLang === "hebrew" || !openDisplaySettings) ?
+ {(Sefaria.interfaceLang === "hebrew") ?
- : }
+ : )}>}
);
}
diff --git a/static/js/ConnectionsPanel.jsx b/static/js/ConnectionsPanel.jsx
index ad23187199..868f3acffa 100644
--- a/static/js/ConnectionsPanel.jsx
+++ b/static/js/ConnectionsPanel.jsx
@@ -17,7 +17,8 @@ import {
} from './Media';
import { CategoryFilter, TextFilter } from './ConnectionFilters';
-import React, { useRef, useState, useEffect } from 'react';
+import React, { useContext, useState, useEffect } from 'react';
+import { ReaderPanelContext } from './context';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import Sefaria from './sefaria/sefaria';
@@ -48,7 +49,7 @@ class ConnectionsPanel extends Component {
this.state = {
flashMessage: null,
currObjectVersions: { en: null, he: null },
- mainVersionLanguage: props.masterPanelLanguage === "bilingual" ? "hebrew" : props.masterPanelLanguage,
+ // mainVersionLanguage: props.masterPanelLanguage === "bilingual" ? "hebrew" : props.masterPanelLanguage,
availableTranslations: [],
linksLoaded: false, // has the list of refs been loaded
connectionSummaryCollapsed: true,
@@ -61,7 +62,7 @@ class ConnectionsPanel extends Component {
componentDidMount() {
this._isMounted = true;
this.loadData();
- this.getCurrentVersions();
+ this.setCurrentVersions();
this.debouncedCheckVisibleSegments = Sefaria.util.debounce(this.checkVisibleSegments, 100);
this.addScrollListener();
}
@@ -88,11 +89,10 @@ class ConnectionsPanel extends Component {
this.props.setConnectionsMode("Resources");
}
- if (prevProps.currVersions.en !== this.props.currVersions.en ||
- prevProps.currVersions.he !== this.props.currVersions.he ||
+ if (!Sefaria.areBothVersionsEqual(prevProps.currVersions, this.props.currVersions) ||
prevProps.masterPanelLanguage !== this.props.masterPanelLanguage ||
prevProps.srefs[0] !== this.props.srefs[0]) {
- this.getCurrentVersions();
+ this.setCurrentVersions();
}
if (prevProps.mode !== this.props.mode || prevProps.connectionsCategory !== this.props.connectionsCategory) {
@@ -183,7 +183,7 @@ class ConnectionsPanel extends Component {
return Sefaria.sectionRef(Sefaria.humanRef(this.props.srefs), true) || this.props.srefs;
}
loadData() {
- let ref = this.sectionRef();
+ let ref = this.props.srefs[0];
if (!Sefaria.related(ref)) {
Sefaria.related(ref, function (data) {
if (this._isMounted) {
@@ -224,51 +224,29 @@ class ConnectionsPanel extends Component {
this.props.setConnectionsMode("Resources");
this.flashMessage("Success! You've created a new connection.");
}
- getData(cb) {
+ async getData() {
// Gets data about this text from cache, which may be null.
const versionPref = Sefaria.versionPreferences.getVersionPref(this.props.srefs[0]);
- return Sefaria.getText(this.props.srefs[0], { context: 1, enVersion: this.props.currVersions.en, heVersion: this.props.currVersions.he, translationLanguagePreference: this.props.translationLanguagePreference, versionPref}).then(cb);
+ return await Sefaria.getTextFromCurrVersions(this.props.srefs[0], this.props.currVersions, this.props.translationLanguagePreference, 1);
}
- getVersionFromData(d, lang) {
- //d - data received from this.getData()
- //language - the language of the version
- //console.log(d);
- const currentVersionTitle = (lang === "he") ? d.heVersionTitle : d.versionTitle;
- return {
- ...d.versions.find(v => v.versionTitle === currentVersionTitle && v.language === lang),
- title: d.indexTitle,
- heTitle: d.heIndexTitle,
- sources: lang === "he" ? d.heSources : d.sources,
- merged: lang === "he" ? !!d.heSources : !!d.sources,
+ async setCurrentVersions() {
+ const data = await this.getData();
+ let currentLanguage = this.props.masterPanelLanguage;
+ if (currentLanguage === "bilingual") {
+ currentLanguage = "hebrew"
}
- }
- getCurrentVersions() {
- const data = this.getData((data) => {
- let currentLanguage = this.props.masterPanelLanguage;
- if (currentLanguage === "bilingual") {
- currentLanguage = "hebrew"
- }
- if (!data || data.error) {
- this.setState({
- currObjectVersions: { en: null, he: null },
- mainVersionLanguage: currentLanguage,
- });
- return
- }
- if (currentLanguage === "hebrew" && !data.he.length) {
- currentLanguage = "english"
- }
- if (currentLanguage === "english" && !data.text.length) {
- currentLanguage = "hebrew"
- }
+ if (!data || data.error) {
this.setState({
- currObjectVersions: {
- en: ((this.props.masterPanelLanguage !== "hebrew" && !!data.text.length) || (this.props.masterPanelLanguage === "hebrew" && !data.he.length)) ? this.getVersionFromData(data, "en") : null,
- he: ((this.props.masterPanelLanguage !== "english" && !!data.he.length) || (this.props.masterPanelLanguage === "english" && !data.text.length)) ? this.getVersionFromData(data, "he") : null,
- },
- mainVersionLanguage: currentLanguage,
- sectionRef: data.sectionRef,
+ currObjectVersions: { en: null, he: null },
});
+ }
+ const [primary, translation] = Sefaria.getPrimaryAndTranslationFromVersions(data.versions);
+ this.setState({
+ currObjectVersions: {
+ en: ((this.props.masterPanelLanguage !== "hebrew" && !!data.text.length) || (this.props.masterPanelLanguage === "hebrew" && !data.he.length)) ? translation : null,
+ he: ((this.props.masterPanelLanguage !== "english" && !!data.he.length) || (this.props.masterPanelLanguage === "english" && !data.text.length)) ? primary : null,
+ },
+ sectionRef: data.sectionRef,
});
}
checkSrefs(srefs) {
@@ -400,7 +378,6 @@ class ConnectionsPanel extends Component {
onCitationClick={this.props.onCitationClick}
handleSheetClick={this.props.handleSheetClick}
openNav={this.props.openNav}
- openDisplaySettings={this.props.openDisplaySettings}
closePanel={this.props.closePanel}
selectedWords={this.props.selectedWords}
checkVisibleSegments={this.checkVisibleSegments}
@@ -417,14 +394,14 @@ class ConnectionsPanel extends Component {
selectedWordsForSheet = null;
} else { // add source from sheet itself
refForSheet = this.props.srefs;
- versionsForSheet = this.props.currVersions;
+ versionsForSheet = this.state.currObjectVersions;
selectedWordsForSheet = this.props.selectedWords;
nodeRef = this.props.nodeRef;
}
content = (
);
- } else if (this.props.mode === "About") {
+ } else if (this.props.mode === "About" || this.props.mode === 'Version Open') {
content = (
{
- const editText = canEditText ? function () {
+ const {textsData} = useContext(ReaderPanelContext);
+ const editText = canEditText && textsData ? function () {
+ const {primaryLang, translationLang} = textsData;
let refString = srefs[0];
let currentPath = Sefaria.util.currentPath();
- let currentLangParam;
- const langCode = masterPanelLanguage.slice(0, 2);
- if (currVersions[langCode]) {
- refString += "/" + encodeURIComponent(langCode) + "/" + encodeURIComponent(currVersions[langCode]);
+ const language = (masterPanelLanguage === 'english') ? translationLang : primaryLang;
+ const langCode = language.slice(0, 2);
+ const currVersionsLangCode = masterPanelLanguage.slice(0, 2);
+ const {versionTitle} = currVersions[currVersionsLangCode];
+ if (versionTitle) {
+ refString += "/" + encodeURIComponent(langCode) + "/" + encodeURIComponent(versionTitle);
}
let path = "/edit/" + refString;
let nextParam = "?next=" + encodeURIComponent(currentPath);
diff --git a/static/js/ConnectionsPanelHeader.jsx b/static/js/ConnectionsPanelHeader.jsx
index 9172a818a9..39cc76ed59 100644
--- a/static/js/ConnectionsPanelHeader.jsx
+++ b/static/js/ConnectionsPanelHeader.jsx
@@ -1,4 +1,4 @@
-import {InterfaceText, EnglishText, HebrewText, LanguageToggleButton, CloseButton } from "./Misc";
+import {InterfaceText, EnglishText, HebrewText, LanguageToggleButton, CloseButton, DisplaySettingsButton} from "./Misc";
import {RecentFilterSet} from "./ConnectionFilters";
import React from 'react';
import ReactDOM from 'react-dom';
@@ -7,6 +7,9 @@ import Sefaria from './sefaria/sefaria';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Component from 'react-class';
+import {ReaderPanelContext} from "./context";
+import {DropdownMenu} from "./common/DropdownMenu";
+import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu";
class ConnectionsPanelHeader extends Component {
@@ -113,13 +116,15 @@ class ConnectionsPanelHeader extends Component {
if (this.props.multiPanel) {
const toggleLang = Sefaria.util.getUrlVars()["lang2"] === "en" ? "he" : "en";
const langUrl = Sefaria.util.replaceUrlParam("lang2", toggleLang);
+ const showOneLanguage = !Sefaria._siteSettings.TORAH_SPECIFIC || Sefaria.interfaceLang === "hebrew";
+ const toggleButton = (showOneLanguage) ? null : (this.props.connectionsMode === 'TextList') ?
+ }> :
+ ;
const closeUrl = Sefaria.util.removeUrlParam("with");
return (
{title}
- {Sefaria.interfaceLang !== "hebrew" && Sefaria._siteSettings.TORAH_SPECIFIC ?
-
- : null }
+ {toggleButton}
);
diff --git a/static/js/ContentText.jsx b/static/js/ContentText.jsx
index 2e0c14049d..41346075ad 100644
--- a/static/js/ContentText.jsx
+++ b/static/js/ContentText.jsx
@@ -2,6 +2,7 @@ import React from "react";
import {useContentLang} from './Hooks';
import Sefaria from './sefaria/sefaria';
import ReactMarkdown from "react-markdown";
+import PropTypes from "prop-types";
const ContentText = (props) => {
/* Renders content language throughout the site (content that comes from the database and is not interface language).
@@ -18,24 +19,28 @@ const ContentText = (props) => {
return langAndContentItems.map(item => );
};
-const VersionContent = (props) => {
- /* Used to render content of Versions.
- * imageLoadCallback is called to update segment numbers placement
- * overrideLanguage a string with the language name (full not 2 letter) to force to render to overriding what the content language context says. Can be useful if calling object determines one langugae is missing in a dynamic way
- * defaultToInterfaceOnBilingual use if you want components not to render all languages in bilingual mode, and default them to what the interface language is
- * See filterContentTextByLang for more documentation */
- const langAndContentItems = _filterContentTextByLang(props);
- const [languageToFilter, _] = useContentLang(props.defaultToInterfaceOnBilingual, props.overrideLanguage);
- return langAndContentItems.map((item) => {
- const [lang, content] = item;
- if (Sefaria.isFullSegmentImage(content)){
- return();
- }
- return ();
- })
+const VersionContent = ({primary, translation, imageLoadCallback}) => {
+ /**
+ * Used to render content of Versions.
+ * imageLoadCallback is called to update segment numbers placement
+ */
+ const versions = {primary, translation};
+ return Object.keys(versions).map((key) => {
+ const version = versions[key];
+ const lang = (version.direction === 'rtl') ? 'he' : 'en';
+ const toFilter = key === 'primary' && !!primary && !!translation;
+ return (Sefaria.isFullSegmentImage(version.text)) ?
+ () :
+ ();
+ });
+}
+VersionContent.propTypes = {
+ primary: PropTypes.object,
+ translation: PropTypes.object,
+ imageLoadCallback: PropTypes.func,
}
-const VersionImageSpan = ({lang, content, languageToFilter, imageLoadCallback}) => {
+const VersionImageSpan = ({lang, content, toFilter, imageLoadCallback}) => {
function getImageAttribute(imgTag, attribute) {
const parser = new DOMParser();
const doc = parser.parseFromString(imgTag, 'text/html');
@@ -50,9 +55,8 @@ const VersionImageSpan = ({lang, content, languageToFilter, imageLoadCallback})
const altText = getImageAttribute(content, 'alt');
const srcText = getImageAttribute(content, 'src');
content = ({
}
{altText}
);
- if (lang === 'he' && languageToFilter === "bilingual") {content = ''}
-
- return({content})
+ if (toFilter) {content = ''}
+ return ({content})
};
const _filterContentTextByLang = ({text, html, markdown, overrideLanguage, defaultToInterfaceOnBilingual=false, bilingualOrder = null}) => {
@@ -77,9 +81,9 @@ const _filterContentTextByLang = ({text, html, markdown, overrideLanguage, defau
return langAndContentItems;
}
-const ContentSpan = ({lang, content, isHTML, markdown}) => {
+const ContentSpan = ({lang, content, isHTML, markdown, primaryOrTranslation}) => {
return isHTML ?
-
+
: markdown ?
{content}
diff --git a/static/js/ExtendedNotes.jsx b/static/js/ExtendedNotes.jsx
index 739385d047..c2b5ba9752 100644
--- a/static/js/ExtendedNotes.jsx
+++ b/static/js/ExtendedNotes.jsx
@@ -13,7 +13,7 @@ class ExtendedNotes extends Component {
this.state = {'notesLanguage': Sefaria.interfaceLang, 'extendedNotes': '', 'langToggle': false};
}
getVersionData(versionList){
- const versionTitle = this.props.currVersions['en'] ? this.props.currVersions['en'] : this.props.currVersions['he'];
+ const versionTitle = this.props.currVersions['en'] ? this.props.currVersions['en'].versionTitle : this.props.currVersions['he'].versionTitle;
const thisVersion = versionList.filter(x=>x.versionTitle===versionTitle)[0];
let extendedNotes = {'english': thisVersion.extendedNotes, 'hebrew': thisVersion.extendedNotesHebrew};
diff --git a/static/js/FontSizeButton.jsx b/static/js/FontSizeButton.jsx
new file mode 100644
index 0000000000..33cd1086ea
--- /dev/null
+++ b/static/js/FontSizeButton.jsx
@@ -0,0 +1,19 @@
+import React, {useContext} from "react";
+import {InterfaceText} from "./Misc";
+import {ReaderPanelContext} from "./context";
+
+function FontSizeButtons() {
+ const {setOption} = useContext(ReaderPanelContext);
+ return (
+
+
+
Font Size
+
+
+ );
+}
+export default FontSizeButtons;
diff --git a/static/js/Header.jsx b/static/js/Header.jsx
index 53ea388954..be88743550 100644
--- a/static/js/Header.jsx
+++ b/static/js/Header.jsx
@@ -40,38 +40,38 @@ const LoggedOutDropdown = () => {
return encodeURIComponent(Sefaria.util.currentPath());
}
return (
- }>
-
-
-
-
-
-
-
-
- Site Language
-
-
-
-
- English
-
-
-
- עברית
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ }>
+
+
+
+
+
+
+
+
+
+ Site Language
+
+
+
+
+ English
+
+
+
+ עברית
+
+
+
+
+
+
+
+
+
+
+
+);
}
@@ -82,71 +82,77 @@ const LoggedInDropdown = () => {
}
return (
-
- }>
-
- {Sefaria.full_name}
-
-
-
- Account Settings
-
-
-
-
-
-
- Site Language
-
-
-
- English
-
- ·
-
- עברית
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+
+ }>
+
+
+ {Sefaria.full_name}
+
+
+
+ Account Settings
+
+
+
+
+
+
+ Site Language
+
+
+
+ English
+
+ ·
+
+ עברית
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
}
const ModuleSwitcher = () => {
return (
- }>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
+ }>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
}
class Header extends Component {
constructor(props) {
diff --git a/static/js/Hooks.jsx b/static/js/Hooks.jsx
index 115812ee73..2cc55521b4 100644
--- a/static/js/Hooks.jsx
+++ b/static/js/Hooks.jsx
@@ -1,6 +1,6 @@
import React, {useState, useEffect, useMemo, useCallback, useRef, useContext} from 'react';
import $ from './sefaria/sefariaJquery';
-import {ContentLanguageContext} from "./context";
+import {ReaderPanelContext} from "./context";
import Sefaria from "./sefaria/sefaria";
@@ -8,8 +8,20 @@ function useContentLang(defaultToInterfaceOnBilingual, overrideLanguage){
/* useful for determining language for content text while taking into account ContentLanguageContent and interfaceLang
* `overrideLanguage` a string with the language name (full not 2 letter) to force to render to overriding what the content language context says. Can be useful if calling object determines one langugae is missing in a dynamic way
* `defaultToInterfaceOnBilingual` use if you want components not to render all languages in bilingual mode, and default them to what the interface language is*/
- const contentLanguage = useContext(ContentLanguageContext);
- const languageToFilter = (defaultToInterfaceOnBilingual && contentLanguage.language === "bilingual") ? Sefaria.interfaceLang : (overrideLanguage ? overrideLanguage : contentLanguage.language);
+ const {language, textsData} = useContext(ReaderPanelContext);
+ const hasContent = !!textsData;
+ const shownLanguage = (language === "bilingual") ? language : (language === "english" && textsData?.text?.length) ? textsData?.translationLang : textsData?.primaryLang; //the 'hebrew' of language means source
+ const isContentLangAmbiguous = !['hebrew', 'english'].includes(shownLanguage);
+ let languageToFilter;
+ if (defaultToInterfaceOnBilingual && hasContent && isContentLangAmbiguous) {
+ languageToFilter = Sefaria.interfaceLang;
+ } else if (overrideLanguage) {
+ languageToFilter = overrideLanguage;
+ } else if (isContentLangAmbiguous || !hasContent) {
+ languageToFilter = language;
+ } else {
+ languageToFilter = shownLanguage;
+ }
const langShort = languageToFilter.slice(0,2);
return [languageToFilter, langShort];
}
diff --git a/static/js/LayoutButtons.jsx b/static/js/LayoutButtons.jsx
new file mode 100644
index 0000000000..e4ab052267
--- /dev/null
+++ b/static/js/LayoutButtons.jsx
@@ -0,0 +1,64 @@
+import {useContext} from "react";
+import {ReaderPanelContext} from "./context";
+import {layoutOptions} from "./constants";
+import {InterfaceText} from "./Misc";
+import PropTypes from "prop-types";
+
+const calculateLayoutState = (language, textsData, panelMode) => {
+ const primaryDir = textsData?.primaryDirection;
+ const translationDir = textsData?.translationDirection;
+ return (language !== 'bilingual') ? 'mono' //one text
+ : (primaryDir !== translationDir || panelMode === 'Sheet') ? 'mixed' //two texts with different directions
+ : (primaryDir === 'rtl') ? 'bi-rtl' //two rtl texts
+ : 'bi-ltr'; //two ltr texts
+};
+
+const getPath = (layoutOption, layoutState, textsData) => {
+ if (layoutState === 'mixed') {
+ const primaryDirection = textsData?.primaryDirection || 'rtl'; //no primary is the case of sheet
+ const translationDirection = textsData?.translationDirection || primaryDirection.split('').reverse().join(''); //when there is an empty translation it has no direction. we will show the button as opposite layouts.
+ const directions = (layoutOption === 'heLeft') ? `${primaryDirection}${translationDirection}` //heLeft means primary in left
+ : `${translationDirection}${primaryDirection}`;
+ if (layoutOption !== 'stacked') {
+ layoutOption = 'beside';
+ }
+ layoutOption = `${layoutOption}-${directions}`;
+ }
+ return `/static/icons/${layoutState}-${layoutOption}.svg`;
+};
+
+const LayoutButton = ({layoutOption, layoutState}) => {
+ const {language, textsData, setOption, layout} = useContext(ReaderPanelContext);
+ const path = getPath(layoutOption, layoutState, textsData);
+ const optionName = (language === 'bilingual') ? 'biLayout' : 'layout';
+ return (
+