@umituz/react-native-settings 4.23.35 β 4.23.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -5
- package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.test.tsx +1 -1
- package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +1 -1
- package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +1 -1
- package/src/domains/localization/domain/repositories/ILocalizationRepository.ts +18 -0
- package/src/domains/localization/index.ts +33 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +40 -0
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +88 -0
- package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
- package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +20 -0
- package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +34 -0
- package/src/domains/localization/infrastructure/config/DeviceLocale.ts +47 -0
- package/src/domains/localization/infrastructure/config/I18nInitializer.ts +73 -0
- package/src/domains/localization/infrastructure/config/LanguageQuery.ts +35 -0
- package/src/domains/localization/infrastructure/config/LocaleMapping.ts +78 -0
- package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +54 -0
- package/src/domains/localization/infrastructure/config/ResourceBuilder.ts +72 -0
- package/src/domains/localization/infrastructure/config/TranslationLoader.ts +46 -0
- package/src/domains/localization/infrastructure/config/__tests__/languagesData.test.ts +69 -0
- package/src/domains/localization/infrastructure/config/constants/defaultLanguages.ts +43 -0
- package/src/domains/localization/infrastructure/config/i18n.ts +9 -0
- package/src/domains/localization/infrastructure/config/languages.ts +28 -0
- package/src/domains/localization/infrastructure/config/languagesData.ts +26 -0
- package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +39 -0
- package/src/domains/localization/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
- package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +44 -0
- package/src/domains/localization/infrastructure/hooks/useLocalization.ts +41 -0
- package/src/domains/localization/infrastructure/hooks/useTranslation.ts +94 -0
- package/src/domains/localization/infrastructure/repository/LanguageRepository.ts +53 -0
- package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +24 -0
- package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +81 -0
- package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +52 -0
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +142 -0
- package/src/domains/localization/infrastructure/storage/types/Language.ts +13 -0
- package/src/domains/localization/infrastructure/storage/types/LocalizationState.ts +27 -0
- package/src/domains/localization/presentation/components/LanguageItem.styles.ts +40 -0
- package/src/domains/localization/presentation/components/LanguageItem.tsx +106 -0
- package/src/domains/localization/presentation/components/LanguageSection.tsx +83 -0
- package/src/domains/localization/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.styles.ts +16 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +132 -0
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +27 -0
- package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +165 -0
- package/src/domains/localization/scripts/prepublish.js +36 -0
- package/src/domains/localization/scripts/setup-languages.js +60 -0
- package/src/domains/localization/scripts/sync-translations.js +124 -0
- package/src/domains/localization/scripts/translate-missing.js +92 -0
- package/src/domains/localization/scripts/utils/file-parser.js +78 -0
- package/src/domains/localization/scripts/utils/key-detector.js +45 -0
- package/src/domains/localization/scripts/utils/key-extractor.js +105 -0
- package/src/domains/localization/scripts/utils/object-helper.js +29 -0
- package/src/domains/localization/scripts/utils/sync-helper.js +49 -0
- package/src/domains/localization/scripts/utils/translation-config.js +116 -0
- package/src/domains/localization/scripts/utils/translator.js +83 -0
- package/src/domains/notifications/presentation/components/NotificationsSection.tsx +1 -1
- package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
- package/src/index.ts +2 -0
- package/src/presentation/components/SettingsErrorBoundary.tsx +1 -1
- package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -1
- package/src/presentation/screens/components/SettingsContent.tsx +1 -1
- package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +1 -1
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +1 -1
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Item Component Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
7
|
+
import { LanguageItem } from '../LanguageItem';
|
|
8
|
+
import type { Language } from '../../infrastructure/storage/types/LocalizationState';
|
|
9
|
+
|
|
10
|
+
const mockLanguage: Language = {
|
|
11
|
+
code: 'en-US',
|
|
12
|
+
name: 'English',
|
|
13
|
+
nativeName: 'English',
|
|
14
|
+
flag: 'πΊπΈ',
|
|
15
|
+
isRTL: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe('LanguageItem', () => {
|
|
19
|
+
const mockOnSelect = jest.fn();
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
mockOnSelect.mockClear();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should render language information correctly', () => {
|
|
26
|
+
const { getAllByText } = render(
|
|
27
|
+
<LanguageItem
|
|
28
|
+
item={mockLanguage}
|
|
29
|
+
isSelected={false}
|
|
30
|
+
onSelect={mockOnSelect}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(getAllByText('English')).toHaveLength(2); // nativeName and name
|
|
35
|
+
expect(getAllByText('πΊπΈ')).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should show check icon when selected', () => {
|
|
39
|
+
const { getByText } = render(
|
|
40
|
+
<LanguageItem
|
|
41
|
+
item={mockLanguage}
|
|
42
|
+
isSelected={true}
|
|
43
|
+
onSelect={mockOnSelect}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(getByText('β')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should not show check icon when not selected', () => {
|
|
51
|
+
const { queryByText } = render(
|
|
52
|
+
<LanguageItem
|
|
53
|
+
item={mockLanguage}
|
|
54
|
+
isSelected={false}
|
|
55
|
+
onSelect={mockOnSelect}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(queryByText('β')).toBeFalsy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should call onSelect when pressed', () => {
|
|
63
|
+
const { getByTestId } = render(
|
|
64
|
+
<LanguageItem
|
|
65
|
+
item={mockLanguage}
|
|
66
|
+
isSelected={false}
|
|
67
|
+
onSelect={mockOnSelect}
|
|
68
|
+
testID="language-item-test"
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
fireEvent.press(getByTestId('language-item-test'));
|
|
73
|
+
expect(mockOnSelect).toHaveBeenCalledWith('en-US');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should use default flag when none provided', () => {
|
|
77
|
+
const languageWithoutFlag = { ...mockLanguage, flag: undefined };
|
|
78
|
+
const { getByText } = render(
|
|
79
|
+
<LanguageItem
|
|
80
|
+
item={languageWithoutFlag}
|
|
81
|
+
isSelected={false}
|
|
82
|
+
onSelect={mockOnSelect}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(getByText('π')).toBeTruthy();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should apply custom styles', () => {
|
|
90
|
+
const customStyles = {
|
|
91
|
+
languageItem: { backgroundColor: 'red' },
|
|
92
|
+
flag: { fontSize: 30 },
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const { getByTestId } = render(
|
|
96
|
+
<LanguageItem
|
|
97
|
+
item={mockLanguage}
|
|
98
|
+
isSelected={false}
|
|
99
|
+
onSelect={mockOnSelect}
|
|
100
|
+
customStyles={customStyles}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(getByTestId('language-item-test')).toBeTruthy();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Screen Styles
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { StyleSheet } from 'react-native';
|
|
6
|
+
|
|
7
|
+
export const styles = StyleSheet.create({
|
|
8
|
+
container: {
|
|
9
|
+
// Styling handled by ScreenLayout
|
|
10
|
+
},
|
|
11
|
+
listContent: {
|
|
12
|
+
// Horizontal padding handled by ScreenLayout contentWrapper
|
|
13
|
+
// Bottom padding handled in component using tokens
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Screen
|
|
3
|
+
* Generic language selector with search functionality
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from 'react';
|
|
7
|
+
import { FlatList } from 'react-native';
|
|
8
|
+
// @ts-ignore - Optional peer dependency
|
|
9
|
+
import {
|
|
10
|
+
useAppDesignTokens,
|
|
11
|
+
SearchBar,
|
|
12
|
+
ScreenLayout,
|
|
13
|
+
NavigationHeader,
|
|
14
|
+
useAppNavigation,
|
|
15
|
+
} from '@umituz/react-native-design-system';
|
|
16
|
+
import { useLanguageSelection } from '../../infrastructure/hooks/useLanguageSelection';
|
|
17
|
+
import { LanguageItem } from '../components/LanguageItem';
|
|
18
|
+
import type { Language } from '../../infrastructure/storage/types/Language';
|
|
19
|
+
import type { LanguageSelectionScreenProps } from './LanguageSelectionScreen.types';
|
|
20
|
+
import { styles } from './LanguageSelectionScreen.styles';
|
|
21
|
+
|
|
22
|
+
export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = ({
|
|
23
|
+
renderLanguageItem,
|
|
24
|
+
renderSearchInput,
|
|
25
|
+
headerTitle,
|
|
26
|
+
onBackPress,
|
|
27
|
+
styles: customStyles,
|
|
28
|
+
searchPlaceholder = "settings.languageSelection.searchPlaceholder",
|
|
29
|
+
testID = 'language-selection-screen',
|
|
30
|
+
}) => {
|
|
31
|
+
const tokens = useAppDesignTokens();
|
|
32
|
+
const navigation = useAppNavigation();
|
|
33
|
+
const {
|
|
34
|
+
searchQuery,
|
|
35
|
+
setSearchQuery,
|
|
36
|
+
selectedCode,
|
|
37
|
+
filteredLanguages,
|
|
38
|
+
handleLanguageSelect,
|
|
39
|
+
} = useLanguageSelection();
|
|
40
|
+
|
|
41
|
+
const onSelect = async (code: string) => {
|
|
42
|
+
if (__DEV__) {
|
|
43
|
+
console.log('[LanguageSelectionScreen] onSelect called with code:', code);
|
|
44
|
+
}
|
|
45
|
+
await handleLanguageSelect(code, () => {
|
|
46
|
+
if (__DEV__) {
|
|
47
|
+
console.log('[LanguageSelectionScreen] Navigating back using context navigation');
|
|
48
|
+
}
|
|
49
|
+
navigation.goBack();
|
|
50
|
+
});
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log('[LanguageSelectionScreen] Language change completed');
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const renderItem = ({ item }: { item: Language }) => {
|
|
57
|
+
const isSelected = selectedCode === item.code;
|
|
58
|
+
|
|
59
|
+
if (renderLanguageItem) {
|
|
60
|
+
return <>{renderLanguageItem(item, isSelected, onSelect)}</>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<LanguageItem
|
|
65
|
+
item={item}
|
|
66
|
+
isSelected={isSelected}
|
|
67
|
+
onSelect={onSelect}
|
|
68
|
+
customStyles={customStyles}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const renderSearchComponent = () => {
|
|
74
|
+
if (renderSearchInput) {
|
|
75
|
+
return renderSearchInput(searchQuery, setSearchQuery, searchPlaceholder);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<SearchBar
|
|
80
|
+
value={searchQuery}
|
|
81
|
+
onChangeText={setSearchQuery}
|
|
82
|
+
placeholder={searchPlaceholder}
|
|
83
|
+
containerStyle={[
|
|
84
|
+
{ marginBottom: tokens.spacing.md },
|
|
85
|
+
customStyles?.searchContainer
|
|
86
|
+
]}
|
|
87
|
+
inputStyle={customStyles?.searchInput}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleBack = () => {
|
|
93
|
+
if (onBackPress) {
|
|
94
|
+
onBackPress();
|
|
95
|
+
} else {
|
|
96
|
+
navigation.goBack();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<ScreenLayout
|
|
102
|
+
testID={testID}
|
|
103
|
+
scrollable={false}
|
|
104
|
+
edges={['top', 'bottom', 'left', 'right']}
|
|
105
|
+
backgroundColor={tokens.colors.backgroundPrimary}
|
|
106
|
+
header={
|
|
107
|
+
<NavigationHeader
|
|
108
|
+
title={headerTitle || ""}
|
|
109
|
+
onBackPress={handleBack}
|
|
110
|
+
/>
|
|
111
|
+
}
|
|
112
|
+
containerStyle={customStyles?.container}
|
|
113
|
+
>
|
|
114
|
+
{renderSearchComponent()}
|
|
115
|
+
<FlatList
|
|
116
|
+
data={filteredLanguages}
|
|
117
|
+
renderItem={renderItem}
|
|
118
|
+
keyExtractor={item => item.code}
|
|
119
|
+
contentContainerStyle={[
|
|
120
|
+
styles.listContent,
|
|
121
|
+
{ paddingBottom: tokens.spacing.xl },
|
|
122
|
+
customStyles?.listContent
|
|
123
|
+
]}
|
|
124
|
+
showsVerticalScrollIndicator={false}
|
|
125
|
+
keyboardShouldPersistTaps="handled"
|
|
126
|
+
/>
|
|
127
|
+
</ScreenLayout>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default LanguageSelectionScreen;
|
|
132
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Screen Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Language } from '../../infrastructure/storage/types/Language';
|
|
6
|
+
|
|
7
|
+
export interface LanguageSelectionScreenProps {
|
|
8
|
+
renderLanguageItem?: (item: Language, isSelected: boolean, onSelect: (code: string) => void) => React.ReactNode;
|
|
9
|
+
renderSearchInput?: (value: string, onChange: (value: string) => void, placeholder: string) => React.ReactNode;
|
|
10
|
+
headerTitle?: string;
|
|
11
|
+
onBackPress?: () => void;
|
|
12
|
+
styles?: {
|
|
13
|
+
container?: object;
|
|
14
|
+
searchContainer?: object;
|
|
15
|
+
languageItem?: object;
|
|
16
|
+
languageContent?: object;
|
|
17
|
+
languageText?: object;
|
|
18
|
+
flag?: object;
|
|
19
|
+
nativeName?: object;
|
|
20
|
+
searchInput?: object;
|
|
21
|
+
searchIcon?: object;
|
|
22
|
+
clearButton?: object;
|
|
23
|
+
listContent?: object;
|
|
24
|
+
};
|
|
25
|
+
searchPlaceholder?: string;
|
|
26
|
+
testID?: string;
|
|
27
|
+
}
|
package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Selection Screen Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
7
|
+
import { LanguageSelectionScreen } from '../LanguageSelectionScreen';
|
|
8
|
+
import { useLocalization } from '../../../infrastructure/hooks/useLocalization';
|
|
9
|
+
import { searchLanguages } from '../../../infrastructure/config/languages';
|
|
10
|
+
|
|
11
|
+
// Mock dependencies
|
|
12
|
+
jest.mock('../../../infrastructure/hooks/useLocalization');
|
|
13
|
+
jest.mock('../../../infrastructure/config/languages');
|
|
14
|
+
jest.mock('@react-navigation/native', () => ({
|
|
15
|
+
useNavigation: () => ({
|
|
16
|
+
goBack: jest.fn(),
|
|
17
|
+
}),
|
|
18
|
+
}), { virtual: true });
|
|
19
|
+
|
|
20
|
+
const mockUseLocalization = useLocalization as jest.MockedFunction<typeof useLocalization>;
|
|
21
|
+
const mockSearchLanguages = searchLanguages as jest.MockedFunction<typeof searchLanguages>;
|
|
22
|
+
|
|
23
|
+
const mockLanguage = {
|
|
24
|
+
code: 'en-US',
|
|
25
|
+
name: 'English',
|
|
26
|
+
nativeName: 'Native English',
|
|
27
|
+
flag: 'πΊπΈ',
|
|
28
|
+
isRTL: false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('LanguageSelectionScreen', () => {
|
|
32
|
+
const mockSetLanguage = jest.fn();
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
mockUseLocalization.mockReturnValue({
|
|
38
|
+
currentLanguage: 'en-US',
|
|
39
|
+
setLanguage: mockSetLanguage,
|
|
40
|
+
t: jest.fn(),
|
|
41
|
+
} as any);
|
|
42
|
+
|
|
43
|
+
mockSearchLanguages.mockReturnValue([mockLanguage]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should render search input and language list', () => {
|
|
47
|
+
const { getByPlaceholderText, getByText } = render(
|
|
48
|
+
<LanguageSelectionScreen
|
|
49
|
+
searchPlaceholder="Search languages..."
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(getByPlaceholderText('Search languages...')).toBeTruthy();
|
|
54
|
+
expect(getByText('English')).toBeTruthy();
|
|
55
|
+
expect(getByText('πΊπΈ')).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should filter languages when searching', () => {
|
|
59
|
+
const { getByPlaceholderText } = render(
|
|
60
|
+
<LanguageSelectionScreen
|
|
61
|
+
searchPlaceholder="Search languages..."
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
fireEvent.changeText(getByPlaceholderText('Search languages...'), 'test');
|
|
66
|
+
expect(mockSearchLanguages).toHaveBeenCalledWith('test');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should select language when item is pressed', async () => {
|
|
70
|
+
const { getByText } = render(
|
|
71
|
+
<LanguageSelectionScreen
|
|
72
|
+
searchPlaceholder="Search languages..."
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fireEvent.press(getByText('English'));
|
|
77
|
+
expect(mockSetLanguage).toHaveBeenCalledWith('en-US');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should show check icon for selected language', () => {
|
|
81
|
+
const { getByText } = render(
|
|
82
|
+
<LanguageSelectionScreen
|
|
83
|
+
searchPlaceholder="Search languages..."
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(getByText('β')).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should use custom render function when provided', () => {
|
|
91
|
+
const customRender = jest.fn().mockReturnValue(<div>Custom Item</div>);
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<LanguageSelectionScreen
|
|
95
|
+
searchPlaceholder="Search languages..."
|
|
96
|
+
renderLanguageItem={customRender}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(customRender).toHaveBeenCalledWith(
|
|
101
|
+
mockLanguage,
|
|
102
|
+
true,
|
|
103
|
+
expect.any(Function)
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should use custom search input when provided', () => {
|
|
108
|
+
const customSearchInput = jest.fn().mockReturnValue(<div>Custom Search</div>);
|
|
109
|
+
|
|
110
|
+
render(
|
|
111
|
+
<LanguageSelectionScreen
|
|
112
|
+
searchPlaceholder="Search languages..."
|
|
113
|
+
renderSearchInput={customSearchInput}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(customSearchInput).toHaveBeenCalledWith(
|
|
118
|
+
'',
|
|
119
|
+
expect.any(Function),
|
|
120
|
+
'Search languages...'
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should use custom container when provided', () => {
|
|
125
|
+
const CustomContainer = ({ children }: { children: React.ReactNode }) => (
|
|
126
|
+
<div testID="custom-container">{children}</div>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const { getByTestId } = render(
|
|
130
|
+
<LanguageSelectionScreen
|
|
131
|
+
searchPlaceholder="Search languages..."
|
|
132
|
+
containerComponent={CustomContainer}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(getByTestId('custom-container')).toBeTruthy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should apply custom styles', () => {
|
|
140
|
+
const customStyles = {
|
|
141
|
+
container: { backgroundColor: 'red' },
|
|
142
|
+
languageItem: { borderColor: 'blue' },
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const { getByTestId } = render(
|
|
146
|
+
<LanguageSelectionScreen
|
|
147
|
+
searchPlaceholder="Search languages..."
|
|
148
|
+
styles={customStyles}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(getByTestId('language-selection-screen')).toHaveStyle({ backgroundColor: 'red' });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should use custom test ID when provided', () => {
|
|
156
|
+
const { getByTestId } = render(
|
|
157
|
+
<LanguageSelectionScreen
|
|
158
|
+
searchPlaceholder="Search languages..."
|
|
159
|
+
testID="custom-test-id"
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(getByTestId('custom-test-id')).toBeTruthy();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-Publish Script
|
|
5
|
+
* Basic checks before publishing
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
16
|
+
const SRC_DIR = path.join(PACKAGE_ROOT, 'src');
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(SRC_DIR)) {
|
|
19
|
+
console.error('β src directory not found');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mainFiles = [
|
|
24
|
+
'src/index.ts',
|
|
25
|
+
'src/infrastructure/config/i18n.ts',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const file of mainFiles) {
|
|
29
|
+
const filePath = path.join(PACKAGE_ROOT, file);
|
|
30
|
+
if (!fs.existsSync(filePath)) {
|
|
31
|
+
console.error(`β Missing mandatory file: ${file}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log('β
Pre-publish checks passed!');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Setup Languages Script
|
|
5
|
+
* Generates index.ts with all available translation files
|
|
6
|
+
* Usage: node setup-languages.js [locales-dir]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
export function setupLanguages(targetDir) {
|
|
13
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(localesDir)) {
|
|
16
|
+
console.error(`β Locales directory not found: ${localesDir}`);
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = fs.readdirSync(localesDir)
|
|
21
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/))
|
|
22
|
+
.sort();
|
|
23
|
+
|
|
24
|
+
const imports = [];
|
|
25
|
+
const exports = [];
|
|
26
|
+
|
|
27
|
+
files.forEach(file => {
|
|
28
|
+
const code = file.replace('.ts', '');
|
|
29
|
+
const varName = code.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()).replace('-', '');
|
|
30
|
+
imports.push(`import ${varName} from "./${code}";`);
|
|
31
|
+
exports.push(` "${code}": ${varName},`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const content = `/**
|
|
35
|
+
* Localization Index
|
|
36
|
+
* Exports all available translation files
|
|
37
|
+
* Auto-generated by scripts/setup-languages.js
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
${imports.join('\n')}
|
|
41
|
+
|
|
42
|
+
export const translations = {
|
|
43
|
+
${exports.join('\n')}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type TranslationKey = keyof typeof translations;
|
|
47
|
+
|
|
48
|
+
export default translations;
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(path.join(localesDir, 'index.ts'), content);
|
|
52
|
+
console.log(`β
Generated index.ts with ${files.length} languages`);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
57
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
58
|
+
console.log('π Setting up language files...\n');
|
|
59
|
+
setupLanguages(targetDir);
|
|
60
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sync Translations Script
|
|
5
|
+
* Synchronizes translation keys from en-US.ts to all other language files
|
|
6
|
+
* Usage: node sync-translations.js [locales-dir] [src-dir-optional]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { parseTypeScriptFile, generateTypeScriptContent } from './utils/file-parser.js';
|
|
12
|
+
import { addMissingKeys, removeExtraKeys } from './utils/sync-helper.js';
|
|
13
|
+
import { detectNewKeys } from './utils/key-detector.js';
|
|
14
|
+
import { extractUsedKeys } from './utils/key-extractor.js';
|
|
15
|
+
import { setDeep } from './utils/object-helper.js';
|
|
16
|
+
|
|
17
|
+
function syncLanguageFile(enUSPath, targetPath, langCode) {
|
|
18
|
+
const enUS = parseTypeScriptFile(enUSPath);
|
|
19
|
+
let target;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
target = parseTypeScriptFile(targetPath);
|
|
23
|
+
} catch {
|
|
24
|
+
target = {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const newKeys = detectNewKeys(enUS, target);
|
|
28
|
+
const addStats = { added: 0, newKeys: [] };
|
|
29
|
+
const removeStats = { removed: 0, removedKeys: [] };
|
|
30
|
+
|
|
31
|
+
addMissingKeys(enUS, target, addStats);
|
|
32
|
+
removeExtraKeys(enUS, target, removeStats);
|
|
33
|
+
|
|
34
|
+
const changed = addStats.added > 0 || removeStats.removed > 0;
|
|
35
|
+
|
|
36
|
+
if (changed) {
|
|
37
|
+
const content = generateTypeScriptContent(target, langCode);
|
|
38
|
+
fs.writeFileSync(targetPath, content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { ...addStats, ...removeStats, newKeys, changed };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function processExtraction(srcDir, enUSPath) {
|
|
45
|
+
if (!srcDir) return;
|
|
46
|
+
|
|
47
|
+
console.log(`π Scanning source code and dependencies: ${srcDir}...`);
|
|
48
|
+
const usedKeyMap = extractUsedKeys(srcDir);
|
|
49
|
+
console.log(` Found ${usedKeyMap.size} unique keys.`);
|
|
50
|
+
|
|
51
|
+
const oldEnUS = parseTypeScriptFile(enUSPath);
|
|
52
|
+
const newEnUS = {};
|
|
53
|
+
|
|
54
|
+
let addedCount = 0;
|
|
55
|
+
for (const [key, defaultValue] of usedKeyMap) {
|
|
56
|
+
// Try to keep existing translation if it exists
|
|
57
|
+
const existingValue = key.split('.').reduce((obj, k) => (obj && obj[k]), oldEnUS);
|
|
58
|
+
|
|
59
|
+
// We treat it as "not translated" if the value is exactly the key string
|
|
60
|
+
const isActuallyTranslated = typeof existingValue === 'string' && existingValue !== key;
|
|
61
|
+
const valueToSet = isActuallyTranslated ? existingValue : defaultValue;
|
|
62
|
+
|
|
63
|
+
if (setDeep(newEnUS, key, valueToSet)) {
|
|
64
|
+
if (!isActuallyTranslated) addedCount++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Count keys in objects
|
|
69
|
+
const getKeysCount = (obj) => {
|
|
70
|
+
let count = 0;
|
|
71
|
+
const walk = (o) => {
|
|
72
|
+
for (const k in o) {
|
|
73
|
+
if (typeof o[k] === 'object' && o[k] !== null) walk(o[k]);
|
|
74
|
+
else count++;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(obj);
|
|
78
|
+
return count;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const oldTotal = getKeysCount(oldEnUS);
|
|
82
|
+
const newTotal = getKeysCount(newEnUS);
|
|
83
|
+
const removedCount = oldTotal - (newTotal - addedCount);
|
|
84
|
+
|
|
85
|
+
console.log(` β¨ Optimized en-US.ts: ${addedCount} keys populated/updated, pruned ${Math.max(0, removedCount)} unused.`);
|
|
86
|
+
const content = generateTypeScriptContent(newEnUS, 'en-US');
|
|
87
|
+
fs.writeFileSync(enUSPath, content);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function syncTranslations(targetDir, srcDir) {
|
|
91
|
+
const localesDir = path.resolve(process.cwd(), targetDir);
|
|
92
|
+
const enUSPath = path.join(localesDir, 'en-US.ts');
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
|
|
95
|
+
console.error(`β Localization files not found in: ${localesDir}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
processExtraction(srcDir, enUSPath);
|
|
100
|
+
|
|
101
|
+
const files = fs.readdirSync(localesDir)
|
|
102
|
+
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
|
|
103
|
+
.sort();
|
|
104
|
+
|
|
105
|
+
console.log(`π Languages to sync: ${files.length}\n`);
|
|
106
|
+
files.forEach(file => {
|
|
107
|
+
const langCode = file.replace('.ts', '');
|
|
108
|
+
const targetPath = path.join(localesDir, file);
|
|
109
|
+
const result = syncLanguageFile(enUSPath, targetPath, langCode);
|
|
110
|
+
if (result.changed) {
|
|
111
|
+
console.log(` π ${langCode}: βοΈ +${result.added} keys, -${result.removed} keys`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
console.log(`\nβ
Synchronization completed!`);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
120
|
+
const targetDir = process.argv[2] || 'src/domains/localization/infrastructure/locales';
|
|
121
|
+
const srcDir = process.argv[3];
|
|
122
|
+
console.log('π Starting translation synchronization...\n');
|
|
123
|
+
syncTranslations(targetDir, srcDir);
|
|
124
|
+
}
|