@umituz/react-native-settings 4.23.35 β†’ 4.23.36

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.
Files changed (66) hide show
  1. package/package.json +12 -5
  2. package/src/domains/appearance/presentation/screens/AppearanceScreen.tsx +1 -1
  3. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.test.tsx +1 -1
  4. package/src/domains/disclaimer/presentation/components/DisclaimerSetting.tsx +1 -1
  5. package/src/domains/disclaimer/presentation/screens/DisclaimerScreen.tsx +1 -1
  6. package/src/domains/localization/domain/repositories/ILocalizationRepository.ts +18 -0
  7. package/src/domains/localization/index.ts +33 -0
  8. package/src/domains/localization/infrastructure/components/LanguageSwitcher.styles.ts +40 -0
  9. package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +88 -0
  10. package/src/domains/localization/infrastructure/components/__tests__/LanguageSwitcher.test.tsx +91 -0
  11. package/src/domains/localization/infrastructure/components/useLanguageNavigation.ts +20 -0
  12. package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +34 -0
  13. package/src/domains/localization/infrastructure/config/DeviceLocale.ts +47 -0
  14. package/src/domains/localization/infrastructure/config/I18nInitializer.ts +73 -0
  15. package/src/domains/localization/infrastructure/config/LanguageQuery.ts +35 -0
  16. package/src/domains/localization/infrastructure/config/LocaleMapping.ts +78 -0
  17. package/src/domains/localization/infrastructure/config/NamespaceResolver.ts +54 -0
  18. package/src/domains/localization/infrastructure/config/ResourceBuilder.ts +72 -0
  19. package/src/domains/localization/infrastructure/config/TranslationLoader.ts +46 -0
  20. package/src/domains/localization/infrastructure/config/__tests__/languagesData.test.ts +69 -0
  21. package/src/domains/localization/infrastructure/config/constants/defaultLanguages.ts +43 -0
  22. package/src/domains/localization/infrastructure/config/i18n.ts +9 -0
  23. package/src/domains/localization/infrastructure/config/languages.ts +28 -0
  24. package/src/domains/localization/infrastructure/config/languagesData.ts +26 -0
  25. package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +39 -0
  26. package/src/domains/localization/infrastructure/hooks/__tests__/useTranslation.test.ts +52 -0
  27. package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +44 -0
  28. package/src/domains/localization/infrastructure/hooks/useLocalization.ts +41 -0
  29. package/src/domains/localization/infrastructure/hooks/useTranslation.ts +94 -0
  30. package/src/domains/localization/infrastructure/repository/LanguageRepository.ts +53 -0
  31. package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +24 -0
  32. package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +81 -0
  33. package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +52 -0
  34. package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +142 -0
  35. package/src/domains/localization/infrastructure/storage/types/Language.ts +13 -0
  36. package/src/domains/localization/infrastructure/storage/types/LocalizationState.ts +27 -0
  37. package/src/domains/localization/presentation/components/LanguageItem.styles.ts +40 -0
  38. package/src/domains/localization/presentation/components/LanguageItem.tsx +106 -0
  39. package/src/domains/localization/presentation/components/LanguageSection.tsx +83 -0
  40. package/src/domains/localization/presentation/components/__tests__/LanguageItem.test.tsx +106 -0
  41. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.styles.ts +16 -0
  42. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +132 -0
  43. package/src/domains/localization/presentation/screens/LanguageSelectionScreen.types.ts +27 -0
  44. package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +165 -0
  45. package/src/domains/localization/scripts/prepublish.js +36 -0
  46. package/src/domains/localization/scripts/setup-languages.js +60 -0
  47. package/src/domains/localization/scripts/sync-translations.js +124 -0
  48. package/src/domains/localization/scripts/translate-missing.js +92 -0
  49. package/src/domains/localization/scripts/utils/file-parser.js +78 -0
  50. package/src/domains/localization/scripts/utils/key-detector.js +45 -0
  51. package/src/domains/localization/scripts/utils/key-extractor.js +105 -0
  52. package/src/domains/localization/scripts/utils/object-helper.js +29 -0
  53. package/src/domains/localization/scripts/utils/sync-helper.js +49 -0
  54. package/src/domains/localization/scripts/utils/translation-config.js +116 -0
  55. package/src/domains/localization/scripts/utils/translator.js +83 -0
  56. package/src/domains/notifications/presentation/components/NotificationsSection.tsx +1 -1
  57. package/src/domains/notifications/presentation/screens/NotificationSettingsScreen.tsx +1 -1
  58. package/src/index.ts +2 -0
  59. package/src/presentation/components/SettingsErrorBoundary.tsx +1 -1
  60. package/src/presentation/navigation/SettingsStackNavigator.tsx +1 -1
  61. package/src/presentation/screens/components/SettingsContent.tsx +1 -1
  62. package/src/presentation/screens/components/SettingsHeader.tsx +1 -1
  63. package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
  64. package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +1 -1
  65. package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +1 -1
  66. 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
+ }
@@ -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
+ }