diff --git a/packages/react-native-web/src/exports/I18nManager/index.js b/packages/react-native-web/src/exports/I18nManager/index.js index 26888aba..7cfed3ad 100644 --- a/packages/react-native-web/src/exports/I18nManager/index.js +++ b/packages/react-native-web/src/exports/I18nManager/index.js @@ -14,11 +14,14 @@ import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'; type I18nManagerStatus = { allowRTL: (allowRTL: boolean) => void, + doLeftAndRightSwapInRTL: boolean, forceRTL: (forceRTL: boolean) => void, + isRTL: boolean, setPreferredLanguageRTL: (setRTL: boolean) => void, - isRTL: boolean + swapLeftAndRightInRTL: (flipStyles: boolean) => void }; +let doLeftAndRightSwapInRTL = true; let isPreferredLanguageRTL = false; let isRTLAllowed = true; let isRTLForced = false; @@ -30,7 +33,7 @@ const isRTL = () => { return isRTLAllowed && isPreferredLanguageRTL; }; -const onChange = () => { +const onDirectionChange = () => { if (ExecutionEnvironment.canUseDOM) { if (document.documentElement && document.documentElement.setAttribute) { document.documentElement.setAttribute('dir', isRTL() ? 'rtl' : 'ltr'); @@ -41,15 +44,21 @@ const onChange = () => { const I18nManager: I18nManagerStatus = { allowRTL(bool) { isRTLAllowed = bool; - onChange(); + onDirectionChange(); }, forceRTL(bool) { isRTLForced = bool; - onChange(); + onDirectionChange(); }, setPreferredLanguageRTL(bool) { isPreferredLanguageRTL = bool; - onChange(); + onDirectionChange(); + }, + swapLeftAndRightInRTL(bool) { + doLeftAndRightSwapInRTL = bool; + }, + get doLeftAndRightSwapInRTL() { + return doLeftAndRightSwapInRTL; }, get isRTL() { return isRTL(); diff --git a/packages/react-native-web/src/exports/StyleSheet/ReactNativeStyleResolver.js b/packages/react-native-web/src/exports/StyleSheet/ReactNativeStyleResolver.js index d53378b0..55e33ea8 100644 --- a/packages/react-native-web/src/exports/StyleSheet/ReactNativeStyleResolver.js +++ b/packages/react-native-web/src/exports/StyleSheet/ReactNativeStyleResolver.js @@ -24,8 +24,8 @@ const emptyObject = {}; export default class ReactNativeStyleResolver { _init() { - this.cache = { ltr: {}, rtl: {} }; - this.injectedCache = { ltr: {}, rtl: {} }; + this.cache = { ltr: {}, rtl: {}, rtlNoSwap: {} }; + this.injectedCache = { ltr: {}, rtl: {}, rtlNoSwap: {} }; this.styleSheetManager = new StyleSheetManager(); } @@ -43,7 +43,8 @@ export default class ReactNativeStyleResolver { } _injectRegisteredStyle(id) { - const dir = I18nManager.isRTL ? 'rtl' : 'ltr'; + const { doLeftAndRightSwapInRTL, isRTL } = I18nManager; + const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr'; if (!this.injectedCache[dir][id]) { const style = flattenStyle(id); const domStyle = createReactDOMStyle(i18nStyle(style)); @@ -120,7 +121,7 @@ export default class ReactNativeStyleResolver { // Create next DOM style props from current and next RN styles const { classList: rdomClassListNext, style: rdomStyleNext } = this.resolve([ - I18nManager.isRTL ? i18nStyle(rnStyle) : rnStyle, + i18nStyle(rnStyle), rnStyleNext ]); @@ -196,7 +197,8 @@ export default class ReactNativeStyleResolver { */ _resolveStyleIfNeeded(style, key) { if (key) { - const dir = I18nManager.isRTL ? 'rtl' : 'ltr'; + const { doLeftAndRightSwapInRTL, isRTL } = I18nManager; + const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr'; if (!this.cache[dir][key]) { // slow: convert style object to props and cache this.cache[dir][key] = this._resolveStyle(style); diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/ReactNativeStyleResolver-test.js b/packages/react-native-web/src/exports/StyleSheet/__tests__/ReactNativeStyleResolver-test.js index 581fa3ba..0d7edaea 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/ReactNativeStyleResolver-test.js +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/ReactNativeStyleResolver-test.js @@ -42,14 +42,22 @@ describe('StyleSheet/ReactNativeStyleResolver', () => { testResolve(a, b, c); }); - test('with register before RTL, resolves to className', () => { + test('with register before RTL, resolves to correct className', () => { const a = ReactNativePropRegistry.register({ left: '12.34%' }); const b = ReactNativePropRegistry.register({ textAlign: 'left' }); const c = ReactNativePropRegistry.register({ marginLeft: 10 }); I18nManager.forceRTL(true); - const resolved = styleResolver.resolve([a, b, c]); + + const resolved1 = styleResolver.resolve([a, b, c]); + expect(resolved1).toMatchSnapshot(); + + I18nManager.swapLeftAndRightInRTL(false); + + const resolved2 = styleResolver.resolve([a, b, c]); + expect(resolved2).toMatchSnapshot(); + + I18nManager.swapLeftAndRightInRTL(true); I18nManager.forceRTL(false); - expect(resolved).toMatchSnapshot(); }); test('with register, resolves to mixed', () => { @@ -102,7 +110,7 @@ describe('StyleSheet/ReactNativeStyleResolver', () => { expect(resolved).toMatchSnapshot(); }); - test('when RTL=true, resolves to flipped inline styles', () => { + test('when isRTL=true, resolves to flipped inline styles', () => { // note: DOM state resolved from { marginLeft: 5, left: 5 } in RTL mode node.style.cssText = 'margin-right: 5px; right: 5px;'; I18nManager.forceRTL(true); @@ -111,8 +119,8 @@ describe('StyleSheet/ReactNativeStyleResolver', () => { expect(resolved).toMatchSnapshot(); }); - test('when RTL=true, resolves to flipped classNames', () => { - // note: DOM state resolved from { marginLeft: 5, left: 5 } in RTL mode + test('when isRTL=true, resolves to flipped classNames', () => { + // note: DOM state resolved from { marginLeft: 5, left: 5 } node.style.cssText = 'margin-right: 5px; right: 5px;'; const nextStyle = ReactNativePropRegistry.register({ marginLeft: 10, right: 1 }); @@ -121,5 +129,19 @@ describe('StyleSheet/ReactNativeStyleResolver', () => { I18nManager.forceRTL(false); expect(resolved).toMatchSnapshot(); }); + + test('when isRTL=true & doLeftAndRightSwapInRTL=false, resolves to non-flipped inline styles', () => { + // note: DOM state resolved from { marginRight 5, right: 5, paddingEnd: 5 } + node.style.cssText = 'margin-right: 5px; right: 5px; padding-left: 5px'; + I18nManager.forceRTL(true); + I18nManager.swapLeftAndRightInRTL(false); + const resolved = styleResolver.resolveWithNode( + { marginRight: 10, right: 10, paddingEnd: 10 }, + node + ); + I18nManager.forceRTL(false); + I18nManager.swapLeftAndRightInRTL(true); + expect(resolved).toMatchSnapshot(); + }); }); }); diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/ReactNativeStyleResolver-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/ReactNativeStyleResolver-test.js.snap index 3e177837..e0b0d0ba 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/ReactNativeStyleResolver-test.js.snap +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/ReactNativeStyleResolver-test.js.snap @@ -9,7 +9,7 @@ Object { } `; -exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to className 1`] = ` +exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 1`] = ` Object { "classList": Array [ "rn-marginRight-zso239", @@ -20,6 +20,17 @@ Object { } `; +exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 2`] = ` +Object { + "classList": Array [ + "rn-left-2s0hu9", + "rn-marginLeft-1n0xq6e", + "rn-textAlign-fdjqy7", + ], + "className": "rn-left-2s0hu9 rn-marginLeft-1n0xq6e rn-textAlign-fdjqy7", +} +`; + exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 1`] = ` Object { "classList": Array [ @@ -246,7 +257,18 @@ Object { } `; -exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when RTL=true, resolves to flipped classNames 1`] = ` +exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true & doLeftAndRightSwapInRTL=false, resolves to non-flipped inline styles 1`] = ` +Object { + "className": "", + "style": Object { + "marginRight": "10px", + "paddingLeft": "10px", + "right": "10px", + }, +} +`; + +exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true, resolves to flipped classNames 1`] = ` Object { "className": "rn-left-1u10d71 rn-marginRight-zso239", "style": Object { @@ -256,7 +278,7 @@ Object { } `; -exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when RTL=true, resolves to flipped inline styles 1`] = ` +exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true, resolves to flipped inline styles 1`] = ` Object { "className": "", "style": Object { diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap deleted file mode 100644 index 4af7401a..00000000 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/i18nStyle-test.js.snap +++ /dev/null @@ -1,105 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`StyleSheet/i18nStyle LTR mode converts and doesn't flip start/end 1`] = ` -Object { - "borderBottomLeftRadius": 20, - "borderBottomRightRadius": "2rem", - "borderLeftColor": "red", - "borderLeftStyle": "solid", - "borderLeftWidth": 5, - "borderRightColor": "blue", - "borderRightStyle": "dotted", - "borderRightWidth": 6, - "borderTopLeftRadius": 10, - "borderTopRightRadius": "1rem", - "left": 1, - "marginLeft": 7, - "marginRight": 8, - "paddingLeft": 9, - "paddingRight": 10, - "right": 2, - "textAlign": "left", - "textShadowOffset": Object { - "height": 10, - "width": "1rem", - }, -} -`; - -exports[`StyleSheet/i18nStyle LTR mode doesn't flip left/right 1`] = ` -Object { - "borderBottomLeftRadius": 20, - "borderBottomRightRadius": "2rem", - "borderLeftColor": "red", - "borderLeftStyle": "solid", - "borderLeftWidth": 5, - "borderRightColor": "blue", - "borderRightStyle": "dotted", - "borderRightWidth": 6, - "borderTopLeftRadius": 10, - "borderTopRightRadius": "1rem", - "left": 1, - "marginLeft": 7, - "marginRight": 8, - "paddingLeft": 9, - "paddingRight": 10, - "right": 2, - "textAlign": "left", - "textShadowOffset": Object { - "height": 10, - "width": "1rem", - }, -} -`; - -exports[`StyleSheet/i18nStyle RTL mode converts and flips start/end 1`] = ` -Object { - "borderBottomLeftRadius": "2rem", - "borderBottomRightRadius": 20, - "borderLeftColor": "blue", - "borderLeftStyle": "dotted", - "borderLeftWidth": 6, - "borderRightColor": "red", - "borderRightStyle": "solid", - "borderRightWidth": 5, - "borderTopLeftRadius": "1rem", - "borderTopRightRadius": 10, - "left": 2, - "marginLeft": 8, - "marginRight": 7, - "paddingLeft": 10, - "paddingRight": 9, - "right": 1, - "textAlign": "right", - "textShadowOffset": Object { - "height": 10, - "width": "-1rem", - }, -} -`; - -exports[`StyleSheet/i18nStyle RTL mode flips left/right 1`] = ` -Object { - "borderBottomLeftRadius": "2rem", - "borderBottomRightRadius": 20, - "borderLeftColor": "blue", - "borderLeftStyle": "dotted", - "borderLeftWidth": 6, - "borderRightColor": "red", - "borderRightStyle": "solid", - "borderRightWidth": 5, - "borderTopLeftRadius": "1rem", - "borderTopRightRadius": 10, - "left": 2, - "marginLeft": 8, - "marginRight": 7, - "paddingLeft": 10, - "paddingRight": 9, - "right": 1, - "textAlign": "right", - "textShadowOffset": Object { - "height": 10, - "width": "-1rem", - }, -} -`; diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js b/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js index 19e0659a..76078812 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/i18nStyle-test.js @@ -3,50 +3,8 @@ import I18nManager from '../../I18nManager'; import i18nStyle from '../i18nStyle'; -const styleLeftRight = { - borderLeftColor: 'red', - borderRightColor: 'blue', - borderTopLeftRadius: 10, - borderTopRightRadius: '1rem', - borderBottomLeftRadius: 20, - borderBottomRightRadius: '2rem', - borderLeftStyle: 'solid', - borderRightStyle: 'dotted', - borderLeftWidth: 5, - borderRightWidth: 6, - left: 1, - marginLeft: 7, - marginRight: 8, - paddingLeft: 9, - paddingRight: 10, - right: 2, - textAlign: 'left', - textShadowOffset: { width: '1rem', height: 10 } -}; - -const styleStartEnd = { - borderStartColor: 'red', - borderEndColor: 'blue', - borderTopStartRadius: 10, - borderTopEndRadius: '1rem', - borderBottomStartRadius: 20, - borderBottomEndRadius: '2rem', - borderStartStyle: 'solid', - borderEndStyle: 'dotted', - borderStartWidth: 5, - borderEndWidth: 6, - start: 1, - marginStart: 7, - marginEnd: 8, - paddingStart: 9, - paddingEnd: 10, - end: 2, - textAlign: 'start', - textShadowOffset: { width: '1rem', height: 10 } -}; - describe('StyleSheet/i18nStyle', () => { - describe('LTR mode', () => { + describe('isRTL = false', () => { beforeEach(() => { I18nManager.allowRTL(false); }); @@ -56,32 +14,59 @@ describe('StyleSheet/i18nStyle', () => { }); test("doesn't flip left/right", () => { - expect(i18nStyle(styleLeftRight)).toMatchSnapshot(); + const initial = { + borderLeftColor: 'red', + left: 1, + marginLeft: 5, + paddingRight: 10, + textAlign: 'right', + textShadowOffset: { width: '1rem', height: 10 } + }; + + expect(i18nStyle(initial)).toEqual(initial); }); test("converts and doesn't flip start/end", () => { - expect(i18nStyle(styleStartEnd)).toMatchSnapshot(); + const initial = { + borderStartColor: 'red', + start: 1, + marginStart: 5, + paddingEnd: 10, + textAlign: 'end', + textShadowOffset: { width: '1rem', height: 10 } + }; + + const expected = { + borderLeftColor: 'red', + left: 1, + marginLeft: 5, + paddingRight: 10, + textAlign: 'right', + textShadowOffset: { width: '1rem', height: 10 } + }; + + expect(i18nStyle(initial)).toEqual(expected); }); test('start/end takes precedence over left/right', () => { - const style = { - borderTopStartRadius: 10, - borderTopLeftRadius: 0, + const initial = { + borderStartWidth: 10, + borderLeftWidth: 0, end: 10, right: 0, marginStart: 10, marginLeft: 0 }; const expected = { - borderTopLeftRadius: 10, + borderLeftWidth: 10, marginLeft: 10, right: 10 }; - expect(i18nStyle(style)).toEqual(expected); + expect(i18nStyle(initial)).toEqual(expected); }); }); - describe('RTL mode', () => { + describe('isRTL = true', () => { beforeEach(() => { I18nManager.forceRTL(true); }); @@ -90,29 +75,125 @@ describe('StyleSheet/i18nStyle', () => { I18nManager.forceRTL(false); }); - test('flips left/right', () => { - expect(i18nStyle(styleLeftRight)).toMatchSnapshot(); + describe('doLeftAndRightSwapInRTL = true', () => { + test('flips left/right', () => { + const initial = { + borderLeftColor: 'red', + left: 1, + marginLeft: 5, + paddingRight: 10, + textAlign: 'right', + textShadowOffset: { width: '1rem', height: 10 } + }; + + const expected = { + borderRightColor: 'red', + right: 1, + marginRight: 5, + paddingLeft: 10, + textAlign: 'left', + textShadowOffset: { width: '-1rem', height: 10 } + }; + + expect(i18nStyle(initial)).toEqual(expected); + }); + + test('converts and flips start/end', () => { + const initial = { + borderStartColor: 'red', + start: 1, + marginStart: 5, + paddingEnd: 10, + textAlign: 'end' + }; + + const expected = { + borderRightColor: 'red', + right: 1, + marginRight: 5, + paddingLeft: 10, + textAlign: 'left' + }; + + expect(i18nStyle(initial)).toEqual(expected); + }); + + test('start/end takes precedence over left/right', () => { + const style = { + borderStartWidth: 10, + borderLeftWidth: 0, + end: 10, + right: 0, + marginStart: 10, + marginLeft: 0 + }; + const expected = { + borderRightWidth: 10, + marginRight: 10, + left: 10 + }; + expect(i18nStyle(style)).toEqual(expected); + }); }); - test('converts and flips start/end', () => { - expect(i18nStyle(styleStartEnd)).toMatchSnapshot(); - }); + describe('doLeftAndRightSwapInRTL = false', () => { + beforeEach(() => { + I18nManager.swapLeftAndRightInRTL(false); + }); - test('start/end takes precedence over left/right', () => { - const style = { - borderTopStartRadius: 10, - borderTopLeftRadius: 0, - end: 10, - right: 0, - marginStart: 10, - marginLeft: 0 - }; - const expected = { - borderTopRightRadius: 10, - marginRight: 10, - left: 10 - }; - expect(i18nStyle(style)).toEqual(expected); + afterEach(() => { + I18nManager.swapLeftAndRightInRTL(true); + }); + + test("doesn't flip left/right", () => { + const initial = { + borderLeftColor: 'red', + left: 1, + marginLeft: 5, + paddingRight: 10, + textAlign: 'right', + textShadowOffset: { width: '1rem', height: 10 } + }; + + expect(i18nStyle(initial)).toEqual(initial); + }); + + test('converts start/end', () => { + const initial = { + borderStartColor: 'red', + start: 1, + marginStart: 5, + paddingEnd: 10, + textAlign: 'end' + }; + + const expected = { + borderRightColor: 'red', + right: 1, + marginRight: 5, + paddingLeft: 10, + textAlign: 'left' + }; + + expect(i18nStyle(initial)).toEqual(expected); + }); + + test('start/end takes precedence over left/right', () => { + const style = { + borderStartWidth: 10, + borderRightWidth: 0, + end: 10, + left: 0, + marginStart: 10, + marginRight: 0 + }; + const expected = { + borderRightWidth: 10, + marginRight: 10, + left: 10 + }; + expect(i18nStyle(style)).toEqual(expected); + }); }); }); }); diff --git a/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js b/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js index 6006311c..6a363d82 100644 --- a/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js +++ b/packages/react-native-web/src/exports/StyleSheet/i18nStyle.js @@ -79,69 +79,52 @@ const PROPERTIES_VALUE = { // Invert the sign of a numeric-like value const additiveInverse = (value: String | Number) => multiplyStyleLengthValue(value, -1); -// Convert I18N properties and values -const convertProperty = (prop: String): String => { - return PROPERTIES_I18N.hasOwnProperty(prop) ? PROPERTIES_I18N[prop] : prop; -}; -const convertValue = (value: String): String => { - return value === 'start' ? 'left' : value === 'end' ? 'right' : value; -}; - -// BiDi flip properties and values -const flipProperty = (prop: String): String => { - return PROPERTIES_FLIP.hasOwnProperty(prop) ? PROPERTIES_FLIP[prop] : prop; -}; -const flipValue = (value: String): String => { - return value === 'left' ? 'right' : value === 'right' ? 'left' : value; -}; - const i18nStyle = originalStyle => { - const isRTL = I18nManager.isRTL; - + const { doLeftAndRightSwapInRTL, isRTL } = I18nManager; const style = originalStyle || emptyObject; - const nextStyle = {}; const frozenProps = {}; + const nextStyle = {}; for (const originalProp in style) { if (!Object.prototype.hasOwnProperty.call(style, originalProp)) { continue; } - + const originalValue = style[originalProp]; let prop = originalProp; - let value = style[originalProp]; - let shouldFreezeProp = false; + let value = originalValue; - // Process I18N properties and values - if (PROPERTIES_I18N[prop]) { - prop = convertProperty(prop); - // I18N properties takes precendence over left/right - shouldFreezeProp = true; - } else if (PROPERTIES_VALUE[prop]) { - value = convertValue(value); + // BiDi flip properties + if (PROPERTIES_I18N.hasOwnProperty(originalProp)) { + // convert start/end + const convertedProp = PROPERTIES_I18N[originalProp]; + prop = isRTL ? PROPERTIES_FLIP[convertedProp] : convertedProp; + } else if (isRTL && doLeftAndRightSwapInRTL && PROPERTIES_FLIP[originalProp]) { + prop = PROPERTIES_FLIP[originalProp]; } - if (isRTL) { - if (PROPERTIES_FLIP[prop]) { - const newProp = flipProperty(prop); - if (!frozenProps[prop]) { - nextStyle[newProp] = value; + // BiDi flip values + if (PROPERTIES_VALUE.hasOwnProperty(originalProp)) { + if (originalValue === 'start') { + value = isRTL ? 'right' : 'left'; + } else if (originalValue === 'end') { + value = isRTL ? 'left' : 'right'; + } else if (isRTL && doLeftAndRightSwapInRTL) { + if (originalValue === 'left') { + value = 'right'; + } else if (originalValue === 'right') { + value = 'left'; } - } else if (PROPERTIES_VALUE[prop]) { - nextStyle[prop] = flipValue(value); - } else if (prop === 'textShadowOffset') { - nextStyle[prop] = value; - nextStyle[prop].width = additiveInverse(value.width); - } else { - nextStyle[prop] = style[prop]; - } - } else { - if (!frozenProps[prop]) { - nextStyle[prop] = value; } } - // Mark the style prop as frozen - if (shouldFreezeProp) { + if (isRTL && prop === 'textShadowOffset') { + nextStyle[prop] = value; + nextStyle[prop].width = additiveInverse(value.width); + } else if (!frozenProps[prop]) { + nextStyle[prop] = value; + } + + if (PROPERTIES_I18N[originalProp]) { frozenProps[prop] = true; } } diff --git a/website/storybook/2-apis/I18nManager/I18nManagerScreen.js b/website/storybook/2-apis/I18nManager/I18nManagerScreen.js index adbd2149..f63ee062 100644 --- a/website/storybook/2-apis/I18nManager/I18nManagerScreen.js +++ b/website/storybook/2-apis/I18nManager/I18nManagerScreen.js @@ -10,33 +10,47 @@ import UIExplorer, { Description, DocItem, Section, storiesOf } from '../../ui-e const I18nManagerScreen = () => ( - Control and set the layout and writing direction of the application. + + Control and query the layout and writing direction of the application. +
+ +
+ +