@umituz/react-native-google-translate 1.0.2 → 1.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-google-translate",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Google Translate integration for React Native apps with rate limiting, batch translation, and TypeScript support",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -9,11 +9,18 @@
9
9
  ".": "./src/index.ts",
10
10
  "./core": "./src/domain/index.ts",
11
11
  "./services": "./src/infrastructure/services/index.ts",
12
+ "./constants": "./src/infrastructure/constants/index.ts",
12
13
  "./hooks": "./src/presentation/hooks/index.ts",
13
- "./package.json": "./package.json"
14
+ "./scripts": "./src/scripts/index.ts",
15
+ "./package.json": "./package.json",
16
+ "./*": "./src/*/index.ts"
14
17
  },
15
18
  "scripts": {
16
- "typecheck": "echo 'TypeScript validation passed'",
19
+ "setup": "node src/scripts/setup.ts",
20
+ "translate": "node src/scripts/translate.ts",
21
+ "sync": "node src/scripts/sync.ts",
22
+ "i18n:setup": "node src/scripts/setup.ts",
23
+ "typecheck": "tsc --noEmit",
17
24
  "lint": "echo 'Lint passed'",
18
25
  "version:patch": "npm version patch -m 'chore: release v%s'",
19
26
  "version:minor": "npm version minor -m 'chore: release v%s'",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Infrastructure Layer
3
+ * @description Subpath: @umituz/react-native-google-translate/infrastructure
4
+ *
5
+ * Exports all infrastructure services, utils, and constants
6
+ */
7
+
8
+ export * from "./services";
9
+ export * from "./utils";
10
+ export * from "./constants";
@@ -75,9 +75,8 @@ class GoogleTranslateService implements ITranslationService {
75
75
  };
76
76
  }
77
77
 
78
- if (this.rateLimiter) {
79
- await this.rateLimiter.waitForSlot();
80
- }
78
+ // After ensureInitialized(), rateLimiter is guaranteed to be non-null
79
+ await this.rateLimiter!.waitForSlot();
81
80
 
82
81
  try {
83
82
  const translatedText = await this.callTranslateAPI(
@@ -1,6 +1,21 @@
1
1
  /**
2
2
  * Infrastructure Services
3
- * @description Exports all services
3
+ * @description Exports all services and utilities
4
4
  */
5
5
 
6
6
  export { googleTranslateService } from "./GoogleTranslate.service";
7
+
8
+ export {
9
+ shouldSkipWord,
10
+ needsTranslation,
11
+ isValidText,
12
+ getTargetLanguage,
13
+ isEnglishVariant,
14
+ getLanguageDisplayName,
15
+ } from "../utils/textValidator.util";
16
+
17
+ export {
18
+ LANGUAGE_MAP,
19
+ SKIP_WORDS,
20
+ LANGUAGE_NAMES,
21
+ } from "../constants/languages.constants";
@@ -59,7 +59,9 @@ export function useBatchTranslation(
59
59
  try {
60
60
  const stats = await googleTranslateService.translateBatch(requests);
61
61
 
62
- setProgress(requests.length);
62
+ // Progress is completed
63
+ setProgress(stats.totalCount);
64
+ options?.onProgress?.(stats.totalCount, stats.totalCount);
63
65
 
64
66
  if (stats.failureCount > 0) {
65
67
  const error = new Error(
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Translation Scripts
3
+ * Scripts for translating and synchronizing localization files
4
+ */
5
+
6
+ export * from './translate';
7
+ export * from './sync';
8
+ export * from './setup';
9
+ export * from './utils';
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Setup Languages Script
5
+ * Creates stub files for all supported languages (if not exist),
6
+ * then generates index.ts from all available translation files.
7
+ * Usage: node setup.ts [locales-dir]
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { LANGUAGE_MAP, getLanguageDisplayName } from '../infrastructure/services';
13
+
14
+ export interface SetupLanguagesOptions {
15
+ targetDir: string;
16
+ }
17
+
18
+ export function setupLanguages(options: SetupLanguagesOptions): boolean {
19
+ const { targetDir } = options;
20
+ const localesDir = path.resolve(process.cwd(), targetDir);
21
+
22
+ if (!fs.existsSync(localesDir)) {
23
+ console.error(`❌ Locales directory not found: ${localesDir}`);
24
+ return false;
25
+ }
26
+
27
+ // Create stub files for all supported languages that don't exist yet
28
+ let created = 0;
29
+ for (const langCode of Object.keys(LANGUAGE_MAP)) {
30
+ // Skip English variants — en-US is the base, others (en-AU, en-GB) are redundant
31
+ if (langCode.startsWith('en-') && langCode !== 'en-US') continue;
32
+
33
+ const filePath = path.join(localesDir, `${langCode}.ts`);
34
+ if (!fs.existsSync(filePath)) {
35
+ const langName = getLanguageDisplayName(langCode);
36
+ fs.writeFileSync(
37
+ filePath,
38
+ `/**\n * ${langName} Translations\n * Auto-synced from en-US.ts\n */\n\nexport default {};\n`,
39
+ );
40
+ console.log(` ✅ Created ${langCode}.ts (${langName})`);
41
+ created++;
42
+ }
43
+ }
44
+
45
+ if (created > 0) {
46
+ console.log(`\n📦 Created ${created} new language stubs.\n`);
47
+ }
48
+
49
+ // Generate index.ts from all language files
50
+ const files = fs.readdirSync(localesDir)
51
+ .filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/))
52
+ .sort();
53
+
54
+ const imports = [];
55
+ const exports = [];
56
+
57
+ files.forEach(file => {
58
+ const code = file.replace('.ts', '');
59
+ const varName = code.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase()).replace('-', '');
60
+ imports.push(`import ${varName} from "./${code}";`);
61
+ exports.push(` "${code}": ${varName},`);
62
+ });
63
+
64
+ const content = `/**
65
+ * Localization Index
66
+ * Exports all available translation files
67
+ * Auto-generated by scripts/setup.ts
68
+ */
69
+
70
+ ${imports.join('\n')}
71
+
72
+ export const translations = {
73
+ ${exports.join('\n')}
74
+ };
75
+
76
+ export type TranslationKey = keyof typeof translations;
77
+
78
+ export default translations;
79
+ `;
80
+
81
+ fs.writeFileSync(path.join(localesDir, 'index.ts'), content);
82
+ console.log(`✅ Generated index.ts with ${files.length} languages`);
83
+ return true;
84
+ }
85
+
86
+ // CLI interface
87
+ export function runSetupLanguages(): void {
88
+ const targetDir = process.argv[2] || 'src/infrastructure/locales';
89
+ console.log('🚀 Setting up language files...\n');
90
+ setupLanguages({ targetDir });
91
+ }
92
+
93
+ if (import.meta.url === `file://${process.argv[1]}`) {
94
+ runSetupLanguages();
95
+ }
@@ -0,0 +1,153 @@
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
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import {
11
+ parseTypeScriptFile,
12
+ generateTypeScriptContent,
13
+ } from './utils/file-parser';
14
+ import {
15
+ addMissingKeys,
16
+ removeExtraKeys,
17
+ type SyncStats,
18
+ } from './utils/sync-helper';
19
+ import { detectNewKeys } from './utils/key-detector';
20
+ import { extractUsedKeys } from './utils/key-extractor';
21
+ import { setDeep, countKeys } from './utils/object-helper';
22
+
23
+ export interface SyncTranslationsOptions {
24
+ targetDir: string;
25
+ srcDir?: string;
26
+ }
27
+
28
+ export interface SyncLanguageFileResult {
29
+ added?: number;
30
+ newKeys?: string[];
31
+ removed?: number;
32
+ removedKeys?: string[];
33
+ changed: boolean;
34
+ detectedNewKeys: unknown[];
35
+ }
36
+
37
+ export function syncLanguageFile(
38
+ enUSPath: string,
39
+ targetPath: string,
40
+ langCode: string
41
+ ): SyncLanguageFileResult {
42
+ const enUS = parseTypeScriptFile(enUSPath);
43
+ let target: Record<string, unknown>;
44
+
45
+ try {
46
+ target = parseTypeScriptFile(targetPath);
47
+ } catch {
48
+ target = {};
49
+ }
50
+
51
+ const detectedNewKeys = detectNewKeys(enUS, target);
52
+ const addStats: SyncStats = { added: 0, newKeys: [] };
53
+ const removeStats: SyncStats = { removed: 0, removedKeys: [] };
54
+
55
+ addMissingKeys(enUS, target, addStats);
56
+ removeExtraKeys(enUS, target, removeStats);
57
+
58
+ const changed = (addStats.added || 0) > 0 || (removeStats.removed || 0) > 0;
59
+
60
+ if (changed) {
61
+ const content = generateTypeScriptContent(target, langCode);
62
+ fs.writeFileSync(targetPath, content);
63
+ }
64
+
65
+ return {
66
+ added: addStats.added,
67
+ newKeys: addStats.newKeys,
68
+ removed: removeStats.removed,
69
+ removedKeys: removeStats.removedKeys,
70
+ changed,
71
+ detectedNewKeys,
72
+ };
73
+ }
74
+
75
+ function processExtraction(srcDir: string | undefined, enUSPath: string): void {
76
+ if (!srcDir) return;
77
+
78
+ console.log(`🔍 Scanning source code and dependencies: ${srcDir}...`);
79
+ const usedKeyMap = extractUsedKeys(srcDir);
80
+ console.log(` Found ${usedKeyMap.size} unique keys.`);
81
+
82
+ const oldEnUS = parseTypeScriptFile(enUSPath);
83
+ const newEnUS: Record<string, unknown> = {};
84
+
85
+ let addedCount = 0;
86
+ for (const [key, defaultValue] of usedKeyMap) {
87
+ // Try to keep existing translation if it exists
88
+ const existingValue = key.split('.').reduce((obj: unknown, k: string) => {
89
+ if (obj && typeof obj === 'object' && k in obj) {
90
+ return (obj as Record<string, unknown>)[k];
91
+ }
92
+ return undefined;
93
+ }, oldEnUS);
94
+
95
+ // We treat it as "not translated" if the value is exactly the key string
96
+ const isActuallyTranslated = typeof existingValue === 'string' && existingValue !== key;
97
+ const valueToSet = isActuallyTranslated ? existingValue : defaultValue;
98
+
99
+ if (setDeep(newEnUS, key, valueToSet)) {
100
+ if (!isActuallyTranslated) addedCount++;
101
+ }
102
+ }
103
+
104
+ const oldTotal = countKeys(oldEnUS);
105
+ const newTotal = countKeys(newEnUS);
106
+ const removedCount = Math.max(0, oldTotal - (newTotal - addedCount));
107
+
108
+ console.log(` ✨ Optimized en-US.ts: ${addedCount} keys populated/updated, pruned ${removedCount} unused.`);
109
+ const content = generateTypeScriptContent(newEnUS, 'en-US');
110
+ fs.writeFileSync(enUSPath, content);
111
+ }
112
+
113
+ export function syncTranslations(options: SyncTranslationsOptions): boolean {
114
+ const { targetDir, srcDir } = options;
115
+ const localesDir = path.resolve(process.cwd(), targetDir);
116
+ const enUSPath = path.join(localesDir, 'en-US.ts');
117
+
118
+ if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
119
+ console.error(`❌ Localization files not found in: ${localesDir}`);
120
+ return false;
121
+ }
122
+
123
+ processExtraction(srcDir, enUSPath);
124
+
125
+ const files = fs.readdirSync(localesDir)
126
+ .filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
127
+ .sort();
128
+
129
+ console.log(`📊 Languages to sync: ${files.length}\n`);
130
+ files.forEach(file => {
131
+ const langCode = file.replace('.ts', '');
132
+ const targetPath = path.join(localesDir, file);
133
+ const result = syncLanguageFile(enUSPath, targetPath, langCode);
134
+ if (result.changed) {
135
+ console.log(` 🌍 ${langCode}: ✏️ +${result.added || 0} keys, -${result.removed || 0} keys`);
136
+ }
137
+ });
138
+
139
+ console.log(`\n✅ Synchronization completed!`);
140
+ return true;
141
+ }
142
+
143
+ // CLI interface
144
+ export function runSyncTranslations(): void {
145
+ const targetDir = process.argv[2] || 'src/infrastructure/locales';
146
+ const srcDir = process.argv[3];
147
+ console.log('🚀 Starting translation synchronization...\n');
148
+ syncTranslations({ targetDir, srcDir });
149
+ }
150
+
151
+ if (import.meta.url === `file://${process.argv[1]}`) {
152
+ runSyncTranslations();
153
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Translate Missing Script
5
+ * Automatically translates missing strings using Google Translate
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import {
11
+ parseTypeScriptFile,
12
+ generateTypeScriptContent,
13
+ } from './utils/file-parser';
14
+ import { googleTranslateService, getTargetLanguage, getLanguageDisplayName } from '../infrastructure/services';
15
+ import type { TranslationStats } from '../domain';
16
+
17
+ // Width of terminal line to clear for progress updates
18
+ const PROGRESS_LINE_WIDTH = 80;
19
+
20
+ export interface TranslateMissingOptions {
21
+ targetDir: string;
22
+ srcDir?: string;
23
+ skipSync?: boolean;
24
+ }
25
+
26
+ export async function translateMissing(options: TranslateMissingOptions): Promise<void> {
27
+ const { targetDir, srcDir, skipSync = false } = options;
28
+
29
+ // Initialize the translation service
30
+ googleTranslateService.initialize({
31
+ minDelay: 100,
32
+ maxRetries: 3,
33
+ timeout: 10000,
34
+ });
35
+
36
+ const localesDir = path.resolve(process.cwd(), targetDir);
37
+ const enUSPath = path.join(localesDir, 'en-US.ts');
38
+
39
+ if (!fs.existsSync(localesDir) || !fs.existsSync(enUSPath)) {
40
+ console.error(`❌ Localization files not found in: ${localesDir}`);
41
+ return;
42
+ }
43
+
44
+ const files = fs.readdirSync(localesDir)
45
+ .filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== 'en-US.ts')
46
+ .sort();
47
+
48
+ console.log(`\n📊 Languages to translate: ${files.length}\n`);
49
+
50
+ const enUS = parseTypeScriptFile(enUSPath);
51
+
52
+ for (const file of files) {
53
+ const langCode = file.replace('.ts', '');
54
+
55
+ // Skip English variants
56
+ const targetLang = getTargetLanguage(langCode);
57
+ if (!targetLang || targetLang === 'en') {
58
+ console.log(`⏭️ Skipping ${langCode} (English variant)`);
59
+ continue;
60
+ }
61
+
62
+ const langName = getLanguageDisplayName(langCode);
63
+ console.log(`🌍 Translating ${langCode} (${langName})...`);
64
+
65
+ const targetPath = path.join(localesDir, file);
66
+ const target = parseTypeScriptFile(targetPath);
67
+
68
+ const stats: TranslationStats = {
69
+ totalCount: 0,
70
+ successCount: 0,
71
+ failureCount: 0,
72
+ skippedCount: 0,
73
+ translatedKeys: [],
74
+ };
75
+
76
+ await googleTranslateService.translateObject(
77
+ enUS,
78
+ target,
79
+ targetLang,
80
+ '',
81
+ stats
82
+ );
83
+
84
+ // Clear progress line
85
+ process.stdout.write('\r' + ' '.repeat(PROGRESS_LINE_WIDTH) + '\r');
86
+
87
+ if (stats.successCount > 0) {
88
+ const content = generateTypeScriptContent(target, langCode);
89
+ fs.writeFileSync(targetPath, content);
90
+
91
+ console.log(` ✅ Successfully translated ${stats.successCount} keys:`);
92
+
93
+ // Detailed logging of translated keys
94
+ const displayCount = Math.min(stats.translatedKeys.length, 15);
95
+ stats.translatedKeys.slice(0, displayCount).forEach(item => {
96
+ console.log(` • ${item.key}: "${item.from}" → "${item.to}"`);
97
+ });
98
+
99
+ if (stats.translatedKeys.length > displayCount) {
100
+ console.log(` ... and ${stats.translatedKeys.length - displayCount} more.`);
101
+ }
102
+ } else {
103
+ console.log(` ✨ Already up to date.`);
104
+ }
105
+ }
106
+
107
+ console.log('\n✅ All translations completed!');
108
+ }
109
+
110
+ // CLI interface
111
+ export function runTranslateMissing(): void {
112
+ const args = process.argv.slice(2).filter(arg => !arg.startsWith('--'));
113
+ const targetDir = args[0] || 'src/infrastructure/locales';
114
+ const srcDir = args[1];
115
+ const skipSync = process.argv.includes('--no-sync');
116
+
117
+ console.log('🚀 Starting integrated translation workflow...');
118
+ translateMissing({ targetDir, srcDir, skipSync }).catch(err => {
119
+ console.error('\n❌ Translation workflow failed:', err.message);
120
+ process.exit(1);
121
+ });
122
+ }
123
+
124
+ if (import.meta.url === `file://${process.argv[1]}`) {
125
+ runTranslateMissing();
126
+ }
@@ -0,0 +1,103 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { getLanguageDisplayName } from '../../infrastructure/utils/textValidator.util';
4
+
5
+ /**
6
+ * File Parser
7
+ * Parse and generate TypeScript translation files
8
+ */
9
+
10
+ export function parseTypeScriptFile(filePath: string): Record<string, unknown> {
11
+ const content = fs.readFileSync(filePath, 'utf8');
12
+
13
+ // Match: export default { ... } OR export const NAME = { ... }
14
+ const match = content.match(/export\s+(?:default|const\s+\w+\s*=)\s*(\{[\s\S]*\});?\s*$/);
15
+
16
+ if (!match) {
17
+ throw new Error(`Could not parse TypeScript file: ${filePath}`);
18
+ }
19
+
20
+ const objectStr = match[1].replace(/;$/, '');
21
+
22
+ try {
23
+ // Basic evaluation for simple objects (safe for generated translation files)
24
+ // Only contains string literals, numbers, booleans, and nested objects
25
+ // eslint-disable-next-line no-eval
26
+ return eval(`(${objectStr})`) as Record<string, unknown>;
27
+ } catch (error) {
28
+ // File might be a barrel file with named imports
29
+ const dir = path.dirname(filePath);
30
+ const importMatches = [...content.matchAll(/import\s*\{\s*(\w+)\s*\}\s*from\s*["']\.\/(\w+)["']/g)];
31
+
32
+ if (importMatches.length > 0) {
33
+ const result: Record<string, unknown> = {};
34
+ for (const [, varName, moduleName] of importMatches) {
35
+ const subFilePath = path.join(dir, `${moduleName}.ts`);
36
+ if (fs.existsSync(subFilePath)) {
37
+ try {
38
+ result[varName] = parseTypeScriptFile(subFilePath);
39
+ } catch (subError) {
40
+ // Log sub-file parse errors but continue with other files
41
+ console.warn(`Warning: Could not parse sub-file ${subFilePath}: ${subError instanceof Error ? subError.message : 'Unknown error'}`);
42
+ }
43
+ }
44
+ }
45
+ if (Object.keys(result).length > 0) {
46
+ return result;
47
+ }
48
+ }
49
+ }
50
+
51
+ return {};
52
+ }
53
+
54
+ export function stringifyValue(value: unknown, indent = 2): string {
55
+ if (typeof value === 'string') {
56
+ const escaped = value
57
+ .replace(/\\/g, '\\\\')
58
+ .replace(/"/g, '\\"')
59
+ .replace(/\n/g, '\\n');
60
+ return `"${escaped}"`;
61
+ }
62
+
63
+ if (Array.isArray(value)) {
64
+ if (value.length === 0) return '[]';
65
+ const items = value.map(v => stringifyValue(v, indent + 2));
66
+ return `[${items.join(', ')}]`;
67
+ }
68
+
69
+ if (typeof value === 'object' && value !== null) {
70
+ const entries = Object.entries(value);
71
+
72
+ if (entries.length === 0) {
73
+ return '{}';
74
+ }
75
+
76
+ const spaces = ' '.repeat(indent);
77
+ const innerSpaces = ' '.repeat(indent + 2);
78
+ const entriesStr = entries
79
+ .sort((a, b) => a[0].localeCompare(b[0]))
80
+ .map(([k, v]) => {
81
+ const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `"${k}"`;
82
+ return `${innerSpaces}${key}: ${stringifyValue(v, indent + 2)}`;
83
+ })
84
+ .join(',\n');
85
+ return `{\n${entriesStr},\n${spaces}}`;
86
+ }
87
+
88
+ return String(value);
89
+ }
90
+
91
+ export function generateTypeScriptContent(obj: Record<string, unknown>, langCode: string): string {
92
+ const langName = getLanguageDisplayName(langCode);
93
+ const isBase = langCode === 'en-US';
94
+ const objString = stringifyValue(obj, 0);
95
+
96
+ return `/**
97
+ * ${langName} Translations
98
+ * ${isBase ? 'Base translations file' : 'Auto-synced from en-US.ts'}
99
+ */
100
+
101
+ export default ${objString};
102
+ `;
103
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Scripts Utils
3
+ * Utility functions for translation scripts
4
+ */
5
+
6
+ export * from './file-parser';
7
+ export * from './key-detector';
8
+ export * from './key-extractor';
9
+ export * from './object-helper';
10
+ export * from './sync-helper';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Key Detector
3
+ * Detects new, missing, and removed keys between source and target objects
4
+ */
5
+
6
+ export interface NewKey {
7
+ path: string;
8
+ value: unknown;
9
+ }
10
+
11
+ export function detectNewKeys(
12
+ sourceObj: Record<string, unknown>,
13
+ targetObj: Record<string, unknown>,
14
+ path = '',
15
+ newKeys: NewKey[] = []
16
+ ): NewKey[] {
17
+ for (const key in sourceObj) {
18
+ const currentPath = path ? `${path}.${key}` : key;
19
+ const sourceValue = sourceObj[key];
20
+ const targetValue = targetObj[key];
21
+
22
+ if (!Object.prototype.hasOwnProperty.call(targetObj, key)) {
23
+ newKeys.push({ path: currentPath, value: sourceValue });
24
+ } else if (
25
+ typeof sourceValue === 'object' &&
26
+ sourceValue !== null &&
27
+ !Array.isArray(sourceValue)
28
+ ) {
29
+ if (typeof targetValue === 'object' && targetValue !== null && !Array.isArray(targetValue)) {
30
+ detectNewKeys(
31
+ sourceValue as Record<string, unknown>,
32
+ targetValue as Record<string, unknown>,
33
+ currentPath,
34
+ newKeys
35
+ );
36
+ }
37
+ }
38
+ }
39
+ return newKeys;
40
+ }
41
+
42
+ export function detectMissingKeys(
43
+ sourceObj: Record<string, unknown>,
44
+ targetObj: Record<string, unknown>,
45
+ path = '',
46
+ missingKeys: string[] = []
47
+ ): string[] {
48
+ for (const key in targetObj) {
49
+ const currentPath = path ? `${path}.${key}` : key;
50
+
51
+ if (!Object.prototype.hasOwnProperty.call(sourceObj, key)) {
52
+ missingKeys.push(currentPath);
53
+ } else if (
54
+ typeof sourceObj[key] === 'object' &&
55
+ sourceObj[key] !== null &&
56
+ !Array.isArray(sourceObj[key]) &&
57
+ typeof targetObj[key] === 'object' &&
58
+ targetObj[key] !== null &&
59
+ !Array.isArray(targetObj[key])
60
+ ) {
61
+ detectMissingKeys(
62
+ sourceObj[key] as Record<string, unknown>,
63
+ targetObj[key] as Record<string, unknown>,
64
+ currentPath,
65
+ missingKeys
66
+ );
67
+ }
68
+ }
69
+ return missingKeys;
70
+ }
@@ -0,0 +1,108 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Generic Key Extractor
6
+ * Scans source code for i18n translation keys
7
+ */
8
+
9
+ const IGNORED_DOMAINS = ['.com', '.org', '.net', '.io', '.co', '.app', '.ai', '.gov', '.edu'];
10
+ const IGNORED_EXTENSIONS = [
11
+ '.ts', '.tsx', '.js', '.jsx', '.json', '.yaml', '.yml',
12
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.pdf',
13
+ '.mp4', '.mov', '.avi', '.mp3', '.wav', '.css', '.scss', '.md'
14
+ ];
15
+ const IGNORED_LAYOUT_VALS = new Set([
16
+ 'center', 'row', 'column', 'flex', 'absolute', 'relative', 'hidden', 'visible',
17
+ 'transparent', 'bold', 'normal', 'italic', 'contain', 'cover', 'stretch',
18
+ 'top', 'bottom', 'left', 'right', 'middle', 'auto', 'none', 'underline',
19
+ 'capitalize', 'uppercase', 'lowercase', 'solid', 'dotted', 'dashed', 'wrap',
20
+ 'nowrap', 'space-between', 'space-around', 'flex-start', 'flex-end', 'baseline',
21
+ 'react', 'index', 'default', 'string', 'number', 'boolean', 'key', 'id'
22
+ ]);
23
+
24
+ function extractFromFile(content: string, keyMap: Map<string, string>): void {
25
+ // Pattern 1: t('key') or t("key")
26
+ const tRegex = /(?:^|\W)t\(['"`]([^'"`]+)['"`]\)/g;
27
+ let match: RegExpExecArray | null;
28
+ while ((match = tRegex.exec(content)) !== null) {
29
+ const key = match[1];
30
+ if (!key.includes('${') && !keyMap.has(key)) {
31
+ keyMap.set(key, key);
32
+ }
33
+ }
34
+
35
+ // Pattern 2: Dot-notation strings (potential i18n keys)
36
+ const dotRegex = /['"`]([a-z][a-z0-9_]*\.(?:[a-z0-9_]+\.)+[a-z0-9_]+)['"`]/gi;
37
+ while ((match = dotRegex.exec(content)) !== null) {
38
+ const key = match[1];
39
+ const isIgnoredDomain = IGNORED_DOMAINS.some(ext => key.toLowerCase().endsWith(ext));
40
+ const isIgnoredExt = IGNORED_EXTENSIONS.some(ext => key.toLowerCase().endsWith(ext));
41
+ if (!isIgnoredDomain && !isIgnoredExt && !key.includes(' ') && !keyMap.has(key)) {
42
+ keyMap.set(key, key);
43
+ }
44
+ }
45
+
46
+ // Pattern 3: Template literals t(`prefix.${var}`)
47
+ const templateRegex = /t\(\`([a-z0-9_.]+)\.\$\{/g;
48
+ while ((match = templateRegex.exec(content)) !== null) {
49
+ const prefix = match[1];
50
+ const arrayMatches = content.matchAll(/\[([\s\S]*?)\]/g);
51
+ for (const arrayMatch of arrayMatches) {
52
+ const inner = arrayMatch[1];
53
+ const idMatches = inner.matchAll(/['"`]([a-z0-9_]{2,40})['"`]/g);
54
+ for (const idMatch of idMatches) {
55
+ const id = idMatch[1];
56
+ if (IGNORED_LAYOUT_VALS.has(id.toLowerCase())) continue;
57
+ if (/^[0-9]+$/.test(id)) continue;
58
+
59
+ const dynamicKey = `${prefix}.${id}`;
60
+ if (!keyMap.has(dynamicKey)) {
61
+ keyMap.set(dynamicKey, dynamicKey);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ function walkDirectory(
69
+ dir: string,
70
+ keyMap: Map<string, string>,
71
+ skipDirs: string[] = ['node_modules', '.expo', '.git', 'build', 'ios', 'android', 'assets', 'locales', '__tests__']
72
+ ): void {
73
+ if (!fs.existsSync(dir)) return;
74
+
75
+ const files = fs.readdirSync(dir);
76
+ for (const file of files) {
77
+ const fullPath = path.join(dir, file);
78
+ const stat = fs.statSync(fullPath);
79
+
80
+ if (stat.isDirectory()) {
81
+ if (!skipDirs.includes(file)) {
82
+ walkDirectory(fullPath, keyMap, skipDirs);
83
+ }
84
+ } else if (/\.(ts|tsx|js|jsx)$/.test(file)) {
85
+ const content = fs.readFileSync(fullPath, 'utf8');
86
+ extractFromFile(content, keyMap);
87
+ }
88
+ }
89
+ }
90
+
91
+ export function extractUsedKeys(srcDir: string): Map<string, string> {
92
+ const keyMap = new Map<string, string>();
93
+ if (!srcDir) return keyMap;
94
+
95
+ const projectRoot = process.cwd();
96
+ const absoluteSrcDir = path.resolve(projectRoot, srcDir);
97
+
98
+ // Scan project source
99
+ walkDirectory(absoluteSrcDir, keyMap);
100
+
101
+ // Scan @umituz packages for shared keys
102
+ const packagesDir = path.resolve(projectRoot, 'node_modules/@umituz');
103
+ if (fs.existsSync(packagesDir)) {
104
+ walkDirectory(packagesDir, keyMap);
105
+ }
106
+
107
+ return keyMap;
108
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Object Helper
3
+ * Utilities for deep object manipulation
4
+ */
5
+
6
+ /**
7
+ * Set a value in a nested object, creating intermediate objects if necessary
8
+ * Returns true if the key was newly added, false if it already existed
9
+ */
10
+ export function setDeep(obj: Record<string, unknown>, path: string, value: unknown): boolean {
11
+ const keys = path.split('.');
12
+ let current = obj;
13
+
14
+ for (let i = 0; i < keys.length - 1; i++) {
15
+ const key = keys[i];
16
+ if (!current[key] || typeof current[key] !== 'object' || Array.isArray(current[key])) {
17
+ current[key] = {};
18
+ }
19
+ current = current[key] as Record<string, unknown>;
20
+ }
21
+
22
+ const lastKey = keys[keys.length - 1];
23
+ if (current[lastKey] === undefined) {
24
+ current[lastKey] = value;
25
+ return true;
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ /**
32
+ * Count all leaf keys in a nested object
33
+ */
34
+ export function countKeys(obj: Record<string, unknown>): number {
35
+ let count = 0;
36
+
37
+ const walk = (o: Record<string, unknown>): void => {
38
+ for (const k in o) {
39
+ if (typeof o[k] === 'object' && o[k] !== null) {
40
+ walk(o[k] as Record<string, unknown>);
41
+ } else {
42
+ count++;
43
+ }
44
+ }
45
+ };
46
+
47
+ walk(obj);
48
+ return count;
49
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Sync Helper
3
+ * Helper functions for synchronizing translation keys
4
+ */
5
+
6
+ export interface SyncStats {
7
+ added?: number;
8
+ newKeys?: string[];
9
+ removed?: number;
10
+ removedKeys?: string[];
11
+ }
12
+
13
+ export function addMissingKeys(
14
+ sourceObj: Record<string, unknown>,
15
+ targetObj: Record<string, unknown>,
16
+ stats: SyncStats = {}
17
+ ): SyncStats {
18
+ stats.added = stats.added || 0;
19
+ stats.newKeys = stats.newKeys || [];
20
+
21
+ for (const key in sourceObj) {
22
+ const sourceValue = sourceObj[key];
23
+ const isNewKey = !Object.prototype.hasOwnProperty.call(targetObj, key);
24
+
25
+ if (isNewKey) {
26
+ targetObj[key] = sourceValue;
27
+ stats.added++;
28
+ stats.newKeys.push(key);
29
+ } else if (
30
+ typeof sourceValue === 'object' &&
31
+ sourceValue !== null &&
32
+ !Array.isArray(sourceValue)
33
+ ) {
34
+ if (!targetObj[key] || typeof targetObj[key] !== 'object') {
35
+ targetObj[key] = {};
36
+ }
37
+ addMissingKeys(
38
+ sourceValue as Record<string, unknown>,
39
+ targetObj[key] as Record<string, unknown>,
40
+ stats
41
+ );
42
+ }
43
+ }
44
+ return stats;
45
+ }
46
+
47
+ export function removeExtraKeys(
48
+ sourceObj: Record<string, unknown>,
49
+ targetObj: Record<string, unknown>,
50
+ stats: SyncStats = {}
51
+ ): SyncStats {
52
+ stats.removed = stats.removed || 0;
53
+ stats.removedKeys = stats.removedKeys || [];
54
+
55
+ for (const key in targetObj) {
56
+ const isExtraKey = !Object.prototype.hasOwnProperty.call(sourceObj, key);
57
+
58
+ if (isExtraKey) {
59
+ delete targetObj[key];
60
+ stats.removed++;
61
+ stats.removedKeys.push(key);
62
+ } else if (
63
+ typeof sourceObj[key] === 'object' &&
64
+ sourceObj[key] !== null &&
65
+ !Array.isArray(sourceObj[key]) &&
66
+ typeof targetObj[key] === 'object' &&
67
+ targetObj[key] !== null &&
68
+ !Array.isArray(targetObj[key])
69
+ ) {
70
+ removeExtraKeys(
71
+ sourceObj[key] as Record<string, unknown>,
72
+ targetObj[key] as Record<string, unknown>,
73
+ stats
74
+ );
75
+ }
76
+ }
77
+ return stats;
78
+ }