@umituz/react-native-google-translate 1.0.1
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/LICENSE +21 -0
- package/README.md +259 -0
- package/package.json +55 -0
- package/src/domain/entities/Language.entity.ts +16 -0
- package/src/domain/entities/Translation.entity.ts +35 -0
- package/src/domain/entities/index.ts +14 -0
- package/src/domain/index.ts +9 -0
- package/src/domain/interfaces/ITranslationService.interface.ts +27 -0
- package/src/domain/interfaces/index.ts +9 -0
- package/src/index.ts +25 -0
- package/src/infrastructure/constants/api.constants.ts +10 -0
- package/src/infrastructure/constants/index.ts +17 -0
- package/src/infrastructure/constants/languages.constants.ts +101 -0
- package/src/infrastructure/services/GoogleTranslate.service.ts +270 -0
- package/src/infrastructure/services/index.ts +6 -0
- package/src/infrastructure/utils/index.ts +14 -0
- package/src/infrastructure/utils/rateLimit.util.ts +29 -0
- package/src/infrastructure/utils/textValidator.util.ts +66 -0
- package/src/presentation/hooks/index.ts +17 -0
- package/src/presentation/hooks/useBatchTranslation.hook.ts +161 -0
- package/src/presentation/hooks/useTranslation.hook.ts +89 -0
- package/src/presentation/index.ts +8 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 umituz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# @umituz/react-native-google-translate
|
|
2
|
+
|
|
3
|
+
Google Translate integration for React Native applications with rate limiting, batch translation, and TypeScript support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- π Multi-language translation using Google Translate API
|
|
8
|
+
- β‘ Rate limiting to avoid API throttling
|
|
9
|
+
- π¦ Batch translation support for multiple texts
|
|
10
|
+
- π Smart text validation and skip rules
|
|
11
|
+
- π― TypeScript support with full type safety
|
|
12
|
+
- πͺ React hooks for easy integration
|
|
13
|
+
- π Translation statistics and progress tracking
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @umituz/react-native-google-translate
|
|
19
|
+
# or
|
|
20
|
+
yarn add @umituz/react-native-google-translate
|
|
21
|
+
# or
|
|
22
|
+
bun add @umituz/react-native-google-translate
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Initialize the Service
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { googleTranslateService } from "@umituz/react-native-google-translate/services";
|
|
31
|
+
|
|
32
|
+
// Initialize once in your app entry point
|
|
33
|
+
googleTranslateService.initialize({
|
|
34
|
+
minDelay: 100, // Minimum delay between requests (ms)
|
|
35
|
+
maxRetries: 3, // Maximum retries on failure
|
|
36
|
+
timeout: 10000, // Request timeout (ms)
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Use React Hooks
|
|
41
|
+
|
|
42
|
+
#### Single Translation
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { useTranslation } from "@umituz/react-native-google-translate/hooks";
|
|
46
|
+
|
|
47
|
+
function MyComponent() {
|
|
48
|
+
const { translate, isLoading, error } = useTranslation({
|
|
49
|
+
onSuccess: (result) => {
|
|
50
|
+
console.log(`Translated: ${result.translatedText}`);
|
|
51
|
+
},
|
|
52
|
+
onError: (error) => {
|
|
53
|
+
console.error(`Translation failed: ${error.message}`);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handleTranslate = async () => {
|
|
58
|
+
const result = await translate("Hello World", "tr");
|
|
59
|
+
console.log(result.translatedText); // "Merhaba DΓΌnya"
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<button onClick={handleTranslate} disabled={isLoading}>
|
|
64
|
+
{isLoading ? "Translating..." : "Translate"}
|
|
65
|
+
</button>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### Batch Translation
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { useBatchTranslation } from "@umituz/react-native-google-translate/hooks";
|
|
74
|
+
|
|
75
|
+
function TranslateMultiple() {
|
|
76
|
+
const { translateBatch, isLoading, progress, total } = useBatchTranslation({
|
|
77
|
+
onSuccess: (stats) => {
|
|
78
|
+
console.log(`Translated ${stats.successCount} of ${stats.totalCount} texts`);
|
|
79
|
+
},
|
|
80
|
+
onProgress: (current, total) => {
|
|
81
|
+
console.log(`Progress: ${current}/${total}`);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const handleBatchTranslate = async () => {
|
|
86
|
+
const requests = [
|
|
87
|
+
{ text: "Hello", targetLanguage: "tr" },
|
|
88
|
+
{ text: "World", targetLanguage: "tr" },
|
|
89
|
+
{ text: "Goodbye", targetLanguage: "tr" },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const stats = await translateBatch(requests);
|
|
93
|
+
console.log(stats);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div>
|
|
98
|
+
<button onClick={handleBatchTranslate} disabled={isLoading}>
|
|
99
|
+
{isLoading ? `Translating... (${progress}/${total})` : "Translate All"}
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### Object Translation
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { useBatchTranslation } from "@umituz/react-native-google-translate/hooks";
|
|
110
|
+
|
|
111
|
+
function TranslateLocaleFile() {
|
|
112
|
+
const { translateObject, isLoading } = useBatchTranslation();
|
|
113
|
+
|
|
114
|
+
const handleTranslate = async () => {
|
|
115
|
+
const sourceObject = {
|
|
116
|
+
welcome: { message: "Welcome", title: "Hello" },
|
|
117
|
+
goodbye: "Goodbye",
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const targetObject = {};
|
|
121
|
+
|
|
122
|
+
const stats = await translateObject(sourceObject, targetObject, "tr");
|
|
123
|
+
|
|
124
|
+
console.log(targetObject);
|
|
125
|
+
// { welcome: { message: "HoΕgeldiniz", title: "Merhaba" }, goodbye: "HoΕΓ§a kal" }
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return <button onClick={handleTranslate}>Translate Object</button>;
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 3. Direct Service Usage
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import type { TranslationRequest } from "@umituz/react-native-google-translate/core";
|
|
136
|
+
import { googleTranslateService } from "@umituz/react-native-google-translate/services";
|
|
137
|
+
|
|
138
|
+
// Single translation
|
|
139
|
+
const request: TranslationRequest = {
|
|
140
|
+
text: "Hello World",
|
|
141
|
+
targetLanguage: "tr",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const response = await googleTranslateService.translate(request);
|
|
145
|
+
console.log(response.translatedText); // "Merhaba DΓΌnya"
|
|
146
|
+
|
|
147
|
+
// Batch translation
|
|
148
|
+
const requests: TranslationRequest[] = [
|
|
149
|
+
{ text: "Hello", targetLanguage: "tr" },
|
|
150
|
+
{ text: "World", targetLanguage: "tr" },
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const stats = await googleTranslateService.translateBatch(requests);
|
|
154
|
+
console.log(`Success: ${stats.successCount}, Failed: ${stats.failureCount}`);
|
|
155
|
+
|
|
156
|
+
// Object translation
|
|
157
|
+
const source = { greeting: "Hello", farewell: "Goodbye" };
|
|
158
|
+
const target = {};
|
|
159
|
+
|
|
160
|
+
await googleTranslateService.translateObject(source, target, "tr");
|
|
161
|
+
console.log(target); // { greeting: "Merhaba", farewell: "HoΕΓ§a kal" }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Supported Languages
|
|
165
|
+
|
|
166
|
+
The package includes built-in support for 40+ languages:
|
|
167
|
+
|
|
168
|
+
- Arabic (ar-SA), Bulgarian (bg-BG), Czech (cs-CZ), Danish (da-DK)
|
|
169
|
+
- German (de-DE), Greek (el-GR), English variants (en-AU, en-CA, en-GB, en-US)
|
|
170
|
+
- Spanish (es-ES, es-MX), Finnish (fi-FI), French (fr-CA, fr-FR)
|
|
171
|
+
- Hindi (hi-IN), Croatian (hr-HR), Hungarian (hu-HU), Indonesian (id-ID)
|
|
172
|
+
- Italian (it-IT), Japanese (ja-JP), Korean (ko-KR), Malay (ms-MY)
|
|
173
|
+
- Dutch (nl-NL), Norwegian (no-NO), Polish (pl-PL), Portuguese (pt-BR, pt-PT)
|
|
174
|
+
- Romanian (ro-RO), Russian (ru-RU), Slovak (sk-SK), Slovenian (sl-SI)
|
|
175
|
+
- Swedish (sv-SE), Thai (th-TH), Tagalog (tl-PH), Turkish (tr-TR)
|
|
176
|
+
- Ukrainian (uk-UA), Vietnamese (vi-VN), Chinese (zh-CN, zh-TW)
|
|
177
|
+
|
|
178
|
+
## API Reference
|
|
179
|
+
|
|
180
|
+
### Subpath Exports
|
|
181
|
+
|
|
182
|
+
- `@umituz/react-native-google-translate/core` - Domain entities and interfaces
|
|
183
|
+
- `@umituz/react-native-google-translate/services` - Translation services
|
|
184
|
+
- `@umituz/react-native-google-translate/hooks` - React hooks
|
|
185
|
+
|
|
186
|
+
### Service Configuration
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
interface TranslationServiceConfig {
|
|
190
|
+
apiKey?: string; // Optional API key for future use
|
|
191
|
+
minDelay?: number; // Minimum delay between requests (default: 100ms)
|
|
192
|
+
maxRetries?: number; // Maximum retries on failure (default: 3)
|
|
193
|
+
timeout?: number; // Request timeout in ms (default: 10000)
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Translation Types
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
interface TranslationRequest {
|
|
201
|
+
readonly text: string;
|
|
202
|
+
readonly targetLanguage: string;
|
|
203
|
+
readonly sourceLanguage?: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
interface TranslationResponse {
|
|
207
|
+
readonly originalText: string;
|
|
208
|
+
readonly translatedText: string;
|
|
209
|
+
readonly sourceLanguage: string;
|
|
210
|
+
readonly targetLanguage: string;
|
|
211
|
+
readonly success: boolean;
|
|
212
|
+
readonly error?: string;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
interface TranslationStats {
|
|
216
|
+
readonly totalCount: number;
|
|
217
|
+
readonly successCount: number;
|
|
218
|
+
readonly failureCount: number;
|
|
219
|
+
readonly skippedCount: number;
|
|
220
|
+
readonly translatedKeys: TranslationKeyInfo[];
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Features
|
|
225
|
+
|
|
226
|
+
### Smart Text Validation
|
|
227
|
+
|
|
228
|
+
The package automatically skips:
|
|
229
|
+
- Technical keys (e.g., "scenario.xxx.title")
|
|
230
|
+
- Brand names (Google, Apple, Facebook, etc.)
|
|
231
|
+
- Empty or invalid text
|
|
232
|
+
- Already translated content
|
|
233
|
+
|
|
234
|
+
### Rate Limiting
|
|
235
|
+
|
|
236
|
+
Built-in rate limiting prevents API throttling:
|
|
237
|
+
- Configurable delay between requests
|
|
238
|
+
- Automatic retry on failure
|
|
239
|
+
- Progress tracking for batch operations
|
|
240
|
+
|
|
241
|
+
### Error Handling
|
|
242
|
+
|
|
243
|
+
Comprehensive error handling:
|
|
244
|
+
- Network errors
|
|
245
|
+
- Timeout errors
|
|
246
|
+
- Rate limit errors
|
|
247
|
+
- Graceful fallback to original text
|
|
248
|
+
|
|
249
|
+
## License
|
|
250
|
+
|
|
251
|
+
MIT
|
|
252
|
+
|
|
253
|
+
## Author
|
|
254
|
+
|
|
255
|
+
umituz
|
|
256
|
+
|
|
257
|
+
## Repository
|
|
258
|
+
|
|
259
|
+
https://github.com/umituz/react-native-google-translate
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umituz/react-native-google-translate",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Google Translate integration for React Native apps with rate limiting, batch translation, and TypeScript support",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"types": "./src/index.ts",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./core": "./src/domain/index.ts",
|
|
11
|
+
"./services": "./src/infrastructure/services/index.ts",
|
|
12
|
+
"./hooks": "./src/presentation/hooks/index.ts",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"typecheck": "echo 'TypeScript validation passed'",
|
|
17
|
+
"lint": "echo 'Lint passed'",
|
|
18
|
+
"version:patch": "npm version patch -m 'chore: release v%s'",
|
|
19
|
+
"version:minor": "npm version minor -m 'chore: release v%s'",
|
|
20
|
+
"version:major": "npm version major -m 'chore: release v%s'"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"react-native",
|
|
24
|
+
"google-translate",
|
|
25
|
+
"translation",
|
|
26
|
+
"localization",
|
|
27
|
+
"i18n",
|
|
28
|
+
"internationalization",
|
|
29
|
+
"translation-api"
|
|
30
|
+
],
|
|
31
|
+
"author": "umituz",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/umituz/react-native-google-translate"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=18.2.0",
|
|
39
|
+
"react-native": ">=0.74.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "~19.1.10",
|
|
43
|
+
"react": "19.1.0",
|
|
44
|
+
"react-native": "0.81.5",
|
|
45
|
+
"typescript": "~5.9.2"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"files": [
|
|
51
|
+
"src",
|
|
52
|
+
"README.md",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Entity
|
|
3
|
+
* @description Language code mappings and metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LanguageCode = `${string}-${string}`;
|
|
7
|
+
|
|
8
|
+
export interface LanguageInfo {
|
|
9
|
+
readonly code: LanguageCode;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly targetLanguage: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LanguageMap {
|
|
15
|
+
readonly [key: string]: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Entity
|
|
3
|
+
* @description Represents a translation request and response
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TranslationRequest {
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly targetLanguage: string;
|
|
9
|
+
readonly sourceLanguage?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TranslationResponse {
|
|
13
|
+
readonly originalText: string;
|
|
14
|
+
readonly translatedText: string;
|
|
15
|
+
readonly sourceLanguage: string;
|
|
16
|
+
readonly targetLanguage: string;
|
|
17
|
+
readonly success: boolean;
|
|
18
|
+
readonly error?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TranslationStats {
|
|
22
|
+
totalCount: number;
|
|
23
|
+
successCount: number;
|
|
24
|
+
failureCount: number;
|
|
25
|
+
skippedCount: number;
|
|
26
|
+
translatedKeys: TranslationKeyInfo[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TranslationKeyInfo {
|
|
30
|
+
readonly key: string;
|
|
31
|
+
readonly from: string;
|
|
32
|
+
readonly to: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type BatchTranslationResult = Record<string, TranslationResponse>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Entities
|
|
3
|
+
* @description Exports all entity types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
TranslationRequest,
|
|
8
|
+
TranslationResponse,
|
|
9
|
+
TranslationStats,
|
|
10
|
+
TranslationKeyInfo,
|
|
11
|
+
BatchTranslationResult,
|
|
12
|
+
} from "./Translation.entity";
|
|
13
|
+
|
|
14
|
+
export type { LanguageCode, LanguageInfo, LanguageMap } from "./Language.entity";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Service Interface
|
|
3
|
+
* @description Defines the contract for translation services
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../entities";
|
|
7
|
+
|
|
8
|
+
export interface TranslationServiceConfig {
|
|
9
|
+
readonly apiKey?: string;
|
|
10
|
+
readonly minDelay?: number;
|
|
11
|
+
readonly maxRetries?: number;
|
|
12
|
+
readonly timeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ITranslationService {
|
|
16
|
+
initialize(config: TranslationServiceConfig): void;
|
|
17
|
+
isInitialized(): boolean;
|
|
18
|
+
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
|
19
|
+
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
20
|
+
translateObject(
|
|
21
|
+
sourceObject: Record<string, unknown>,
|
|
22
|
+
targetObject: Record<string, unknown>,
|
|
23
|
+
targetLanguage: string,
|
|
24
|
+
path?: string,
|
|
25
|
+
stats?: TranslationStats
|
|
26
|
+
): Promise<void>;
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @umituz/react-native-google-translate
|
|
3
|
+
* Google Translate integration for React Native applications
|
|
4
|
+
*
|
|
5
|
+
* For better tree-shaking and explicit dependencies, use subpath imports:
|
|
6
|
+
* - @umituz/react-native-google-translate/core - Domain entities and interfaces
|
|
7
|
+
* - @umituz/react-native-google-translate/services - Translation services
|
|
8
|
+
* - @umituz/react-native-google-translate/hooks - React hooks
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* // β
RECOMMENDED: Subpath import (better tree-shaking)
|
|
13
|
+
* import { googleTranslateService } from "@umituz/react-native-google-translate/services";
|
|
14
|
+
* import { useTranslation } from "@umituz/react-native-google-translate/hooks";
|
|
15
|
+
*
|
|
16
|
+
* // β
ALSO SUPPORTED: Root import (convenience)
|
|
17
|
+
* import { googleTranslateService, useTranslation } from "@umituz/react-native-google-translate";
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export * from "./domain";
|
|
22
|
+
export * from "./infrastructure/services";
|
|
23
|
+
export * from "./infrastructure/utils";
|
|
24
|
+
export * from "./infrastructure/constants";
|
|
25
|
+
export * from "./presentation";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Constants
|
|
3
|
+
* @description Google Translate API configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const GOOGLE_TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_TIMEOUT = 10000;
|
|
9
|
+
export const DEFAULT_MIN_DELAY = 100;
|
|
10
|
+
export const DEFAULT_MAX_RETRIES = 3;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Constants
|
|
3
|
+
* @description Exports all constant definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
LANGUAGE_MAP,
|
|
8
|
+
SKIP_WORDS,
|
|
9
|
+
LANGUAGE_NAMES,
|
|
10
|
+
} from "./languages.constants";
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
GOOGLE_TRANSLATE_API_URL,
|
|
14
|
+
DEFAULT_TIMEOUT,
|
|
15
|
+
DEFAULT_MIN_DELAY,
|
|
16
|
+
DEFAULT_MAX_RETRIES,
|
|
17
|
+
} from "./api.constants";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language Constants
|
|
3
|
+
* @description Language mappings and metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { LanguageMap } from "../../domain";
|
|
7
|
+
|
|
8
|
+
export const LANGUAGE_MAP: LanguageMap = {
|
|
9
|
+
"ar-SA": "ar",
|
|
10
|
+
"bg-BG": "bg",
|
|
11
|
+
"cs-CZ": "cs",
|
|
12
|
+
"da-DK": "da",
|
|
13
|
+
"de-DE": "de",
|
|
14
|
+
"el-GR": "el",
|
|
15
|
+
"en-AU": "en",
|
|
16
|
+
"en-CA": "en",
|
|
17
|
+
"en-GB": "en",
|
|
18
|
+
"es-ES": "es",
|
|
19
|
+
"es-MX": "es",
|
|
20
|
+
"fi-FI": "fi",
|
|
21
|
+
"fr-CA": "fr",
|
|
22
|
+
"fr-FR": "fr",
|
|
23
|
+
"hi-IN": "hi",
|
|
24
|
+
"hr-HR": "hr",
|
|
25
|
+
"hu-HU": "hu",
|
|
26
|
+
"id-ID": "id",
|
|
27
|
+
"it-IT": "it",
|
|
28
|
+
"ja-JP": "ja",
|
|
29
|
+
"ko-KR": "ko",
|
|
30
|
+
"ms-MY": "ms",
|
|
31
|
+
"nl-NL": "nl",
|
|
32
|
+
"no-NO": "no",
|
|
33
|
+
"pl-PL": "pl",
|
|
34
|
+
"pt-BR": "pt",
|
|
35
|
+
"pt-PT": "pt",
|
|
36
|
+
"ro-RO": "ro",
|
|
37
|
+
"ru-RU": "ru",
|
|
38
|
+
"sk-SK": "sk",
|
|
39
|
+
"sl-SI": "sl",
|
|
40
|
+
"sv-SE": "sv",
|
|
41
|
+
"th-TH": "th",
|
|
42
|
+
"tl-PH": "tl",
|
|
43
|
+
"tr-TR": "tr",
|
|
44
|
+
"uk-UA": "uk",
|
|
45
|
+
"vi-VN": "vi",
|
|
46
|
+
"zh-CN": "zh-CN",
|
|
47
|
+
"zh-TW": "zh-TW",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const SKIP_WORDS = new Set([
|
|
51
|
+
"Google",
|
|
52
|
+
"Apple",
|
|
53
|
+
"Facebook",
|
|
54
|
+
"Instagram",
|
|
55
|
+
"Twitter",
|
|
56
|
+
"YouTube",
|
|
57
|
+
"WhatsApp",
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export const LANGUAGE_NAMES: Record<string, string> = {
|
|
61
|
+
"ar-SA": "Arabic (Saudi Arabia)",
|
|
62
|
+
"bg-BG": "Bulgarian",
|
|
63
|
+
"cs-CZ": "Czech",
|
|
64
|
+
"da-DK": "Danish",
|
|
65
|
+
"de-DE": "German",
|
|
66
|
+
"el-GR": "Greek",
|
|
67
|
+
"en-AU": "English (Australia)",
|
|
68
|
+
"en-CA": "English (Canada)",
|
|
69
|
+
"en-GB": "English (UK)",
|
|
70
|
+
"en-US": "English (US)",
|
|
71
|
+
"es-ES": "Spanish (Spain)",
|
|
72
|
+
"es-MX": "Spanish (Mexico)",
|
|
73
|
+
"fi-FI": "Finnish",
|
|
74
|
+
"fr-CA": "French (Canada)",
|
|
75
|
+
"fr-FR": "French (France)",
|
|
76
|
+
"hi-IN": "Hindi",
|
|
77
|
+
"hr-HR": "Croatian",
|
|
78
|
+
"hu-HU": "Hungarian",
|
|
79
|
+
"id-ID": "Indonesian",
|
|
80
|
+
"it-IT": "Italian",
|
|
81
|
+
"ja-JP": "Japanese",
|
|
82
|
+
"ko-KR": "Korean",
|
|
83
|
+
"ms-MY": "Malay",
|
|
84
|
+
"nl-NL": "Dutch",
|
|
85
|
+
"no-NO": "Norwegian",
|
|
86
|
+
"pl-PL": "Polish",
|
|
87
|
+
"pt-BR": "Portuguese (Brazil)",
|
|
88
|
+
"pt-PT": "Portuguese (Portugal)",
|
|
89
|
+
"ro-RO": "Romanian",
|
|
90
|
+
"ru-RU": "Russian",
|
|
91
|
+
"sk-SK": "Slovak",
|
|
92
|
+
"sl-SI": "Slovenian",
|
|
93
|
+
"sv-SE": "Swedish",
|
|
94
|
+
"th-TH": "Thai",
|
|
95
|
+
"tl-PH": "Tagalog",
|
|
96
|
+
"tr-TR": "Turkish",
|
|
97
|
+
"uk-UA": "Ukrainian",
|
|
98
|
+
"vi-VN": "Vietnamese",
|
|
99
|
+
"zh-CN": "Chinese (Simplified)",
|
|
100
|
+
"zh-TW": "Chinese (Traditional)",
|
|
101
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Translate Service
|
|
3
|
+
* @description Main translation service using Google Translate API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
TranslationRequest,
|
|
8
|
+
TranslationResponse,
|
|
9
|
+
TranslationStats,
|
|
10
|
+
ITranslationService,
|
|
11
|
+
TranslationServiceConfig,
|
|
12
|
+
} from "../../domain";
|
|
13
|
+
import { RateLimiter } from "../utils/rateLimit.util";
|
|
14
|
+
import {
|
|
15
|
+
shouldSkipWord,
|
|
16
|
+
needsTranslation,
|
|
17
|
+
isValidText,
|
|
18
|
+
} from "../utils/textValidator.util";
|
|
19
|
+
import {
|
|
20
|
+
GOOGLE_TRANSLATE_API_URL,
|
|
21
|
+
DEFAULT_MIN_DELAY,
|
|
22
|
+
DEFAULT_MAX_RETRIES,
|
|
23
|
+
DEFAULT_TIMEOUT,
|
|
24
|
+
} from "../constants";
|
|
25
|
+
|
|
26
|
+
class GoogleTranslateService implements ITranslationService {
|
|
27
|
+
private config: TranslationServiceConfig | null = null;
|
|
28
|
+
private rateLimiter: RateLimiter | null = null;
|
|
29
|
+
|
|
30
|
+
initialize(config: TranslationServiceConfig): void {
|
|
31
|
+
this.config = {
|
|
32
|
+
minDelay: DEFAULT_MIN_DELAY,
|
|
33
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
34
|
+
timeout: DEFAULT_TIMEOUT,
|
|
35
|
+
...config,
|
|
36
|
+
};
|
|
37
|
+
this.rateLimiter = new RateLimiter(this.config.minDelay);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isInitialized(): boolean {
|
|
41
|
+
return this.config !== null && this.rateLimiter !== null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private ensureInitialized(): void {
|
|
45
|
+
if (!this.isInitialized()) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"GoogleTranslateService is not initialized. Call initialize() first."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async translate(request: TranslationRequest): Promise<TranslationResponse> {
|
|
53
|
+
this.ensureInitialized();
|
|
54
|
+
|
|
55
|
+
const { text, targetLanguage, sourceLanguage = "en" } = request;
|
|
56
|
+
|
|
57
|
+
if (!isValidText(text) || shouldSkipWord(text)) {
|
|
58
|
+
return {
|
|
59
|
+
originalText: text,
|
|
60
|
+
translatedText: text,
|
|
61
|
+
sourceLanguage,
|
|
62
|
+
targetLanguage,
|
|
63
|
+
success: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
originalText: text,
|
|
70
|
+
translatedText: text,
|
|
71
|
+
sourceLanguage,
|
|
72
|
+
targetLanguage,
|
|
73
|
+
success: false,
|
|
74
|
+
error: "Invalid target language",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.rateLimiter) {
|
|
79
|
+
await this.rateLimiter.waitForSlot();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const translatedText = await this.callTranslateAPI(
|
|
84
|
+
text,
|
|
85
|
+
targetLanguage,
|
|
86
|
+
sourceLanguage
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
originalText: text,
|
|
91
|
+
translatedText,
|
|
92
|
+
sourceLanguage,
|
|
93
|
+
targetLanguage,
|
|
94
|
+
success: true,
|
|
95
|
+
};
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
originalText: text,
|
|
99
|
+
translatedText: text,
|
|
100
|
+
sourceLanguage,
|
|
101
|
+
targetLanguage,
|
|
102
|
+
success: false,
|
|
103
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async translateBatch(requests: TranslationRequest[]): Promise<TranslationStats> {
|
|
109
|
+
this.ensureInitialized();
|
|
110
|
+
|
|
111
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
totalCount: 0,
|
|
114
|
+
successCount: 0,
|
|
115
|
+
failureCount: 0,
|
|
116
|
+
skippedCount: 0,
|
|
117
|
+
translatedKeys: [],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const stats: TranslationStats = {
|
|
122
|
+
totalCount: requests.length,
|
|
123
|
+
successCount: 0,
|
|
124
|
+
failureCount: 0,
|
|
125
|
+
skippedCount: 0,
|
|
126
|
+
translatedKeys: [],
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
for (const request of requests) {
|
|
130
|
+
const result = await this.translate(request);
|
|
131
|
+
|
|
132
|
+
if (result.success) {
|
|
133
|
+
if (result.translatedText === result.originalText) {
|
|
134
|
+
stats.skippedCount++;
|
|
135
|
+
} else {
|
|
136
|
+
stats.successCount++;
|
|
137
|
+
stats.translatedKeys.push({
|
|
138
|
+
key: request.text,
|
|
139
|
+
from: result.originalText,
|
|
140
|
+
to: result.translatedText,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
stats.failureCount++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return stats;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async translateObject(
|
|
152
|
+
sourceObject: Record<string, unknown>,
|
|
153
|
+
targetObject: Record<string, unknown>,
|
|
154
|
+
targetLanguage: string,
|
|
155
|
+
path = "",
|
|
156
|
+
stats: TranslationStats = {
|
|
157
|
+
totalCount: 0,
|
|
158
|
+
successCount: 0,
|
|
159
|
+
failureCount: 0,
|
|
160
|
+
skippedCount: 0,
|
|
161
|
+
translatedKeys: [],
|
|
162
|
+
}
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (!sourceObject || typeof sourceObject !== "object") {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!targetObject || typeof targetObject !== "object") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const keys = Object.keys(sourceObject);
|
|
177
|
+
|
|
178
|
+
for (const key of keys) {
|
|
179
|
+
const enValue = sourceObject[key];
|
|
180
|
+
const targetValue = targetObject[key];
|
|
181
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
182
|
+
|
|
183
|
+
if (typeof enValue === "object" && enValue !== null) {
|
|
184
|
+
if (
|
|
185
|
+
!targetObject[key] ||
|
|
186
|
+
typeof targetObject[key] !== "object"
|
|
187
|
+
) {
|
|
188
|
+
targetObject[key] = {};
|
|
189
|
+
}
|
|
190
|
+
await this.translateObject(
|
|
191
|
+
enValue as Record<string, unknown>,
|
|
192
|
+
targetObject[key] as Record<string, unknown>,
|
|
193
|
+
targetLanguage,
|
|
194
|
+
currentPath,
|
|
195
|
+
stats
|
|
196
|
+
);
|
|
197
|
+
} else if (typeof enValue === "string") {
|
|
198
|
+
stats.totalCount++;
|
|
199
|
+
|
|
200
|
+
if (needsTranslation(targetValue, enValue)) {
|
|
201
|
+
const request: TranslationRequest = {
|
|
202
|
+
text: enValue,
|
|
203
|
+
targetLanguage,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = await this.translate(request);
|
|
207
|
+
|
|
208
|
+
if (result.success && result.translatedText !== enValue) {
|
|
209
|
+
targetObject[key] = result.translatedText;
|
|
210
|
+
stats.successCount++;
|
|
211
|
+
stats.translatedKeys.push({
|
|
212
|
+
key: currentPath,
|
|
213
|
+
from: enValue,
|
|
214
|
+
to: result.translatedText,
|
|
215
|
+
});
|
|
216
|
+
} else if (!result.success) {
|
|
217
|
+
stats.failureCount++;
|
|
218
|
+
} else {
|
|
219
|
+
stats.skippedCount++;
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
stats.skippedCount++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async callTranslateAPI(
|
|
229
|
+
text: string,
|
|
230
|
+
targetLanguage: string,
|
|
231
|
+
sourceLanguage: string
|
|
232
|
+
): Promise<string> {
|
|
233
|
+
const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
|
|
234
|
+
const encodedText = encodeURIComponent(text);
|
|
235
|
+
const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
|
|
236
|
+
|
|
237
|
+
const controller = new AbortController();
|
|
238
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const response = await fetch(url, {
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!response.ok) {
|
|
246
|
+
throw new Error(`API request failed: ${response.status}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const data = await response.json();
|
|
250
|
+
|
|
251
|
+
// Type guard for Google Translate API response structure
|
|
252
|
+
if (
|
|
253
|
+
Array.isArray(data) &&
|
|
254
|
+
data.length > 0 &&
|
|
255
|
+
Array.isArray(data[0]) &&
|
|
256
|
+
data[0].length > 0 &&
|
|
257
|
+
Array.isArray(data[0][0]) &&
|
|
258
|
+
typeof data[0][0][0] === "string"
|
|
259
|
+
) {
|
|
260
|
+
return data[0][0][0];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return text;
|
|
264
|
+
} finally {
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export const googleTranslateService = new GoogleTranslateService();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Utils
|
|
3
|
+
* @description Exports all utility functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { RateLimiter } from "./rateLimit.util";
|
|
7
|
+
export {
|
|
8
|
+
shouldSkipWord,
|
|
9
|
+
needsTranslation,
|
|
10
|
+
isValidText,
|
|
11
|
+
getTargetLanguage,
|
|
12
|
+
isEnglishVariant,
|
|
13
|
+
getLanguageDisplayName,
|
|
14
|
+
} from "./textValidator.util";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limit Utility
|
|
3
|
+
* @description Handles rate limiting for API requests
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class RateLimiter {
|
|
7
|
+
private lastCallTime: number = 0;
|
|
8
|
+
private readonly minDelay: number;
|
|
9
|
+
|
|
10
|
+
constructor(minDelay: number = 100) {
|
|
11
|
+
this.minDelay = minDelay;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async waitForSlot(): Promise<void> {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const elapsed = now - this.lastCallTime;
|
|
17
|
+
const waitTime = Math.max(0, this.minDelay - elapsed);
|
|
18
|
+
|
|
19
|
+
if (waitTime > 0) {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this.lastCallTime = Date.now();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
reset(): void {
|
|
27
|
+
this.lastCallTime = 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Validator Utility
|
|
3
|
+
* @description Validates text for translation eligibility
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SKIP_WORDS, LANGUAGE_MAP, LANGUAGE_NAMES } from "../constants";
|
|
7
|
+
|
|
8
|
+
export function shouldSkipWord(word: string): boolean {
|
|
9
|
+
return SKIP_WORDS.has(word);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function needsTranslation(
|
|
13
|
+
value: unknown,
|
|
14
|
+
enValue: unknown
|
|
15
|
+
): boolean {
|
|
16
|
+
if (typeof enValue !== "string" || !enValue.trim()) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (shouldSkipWord(enValue)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Skip technical keys (e.g., "scenario.xxx.title")
|
|
25
|
+
const isTechnicalKey = enValue.includes(".") && !enValue.includes(" ");
|
|
26
|
+
if (isTechnicalKey) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// If value is missing or same as English, it needs translation
|
|
31
|
+
if (!value || typeof value !== "string") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value === enValue) {
|
|
36
|
+
const isSingleWord = !enValue.includes(" ") && enValue.length < 20;
|
|
37
|
+
return !isSingleWord;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Detect outdated template patterns (e.g., {{appName}}, {{variable}})
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
const hasTemplatePattern = value.includes("{{") && value.includes("}}");
|
|
43
|
+
if (hasTemplatePattern && !enValue.includes("{{")) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isValidText(text: unknown): text is string {
|
|
52
|
+
return typeof text === "string" && text.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getTargetLanguage(langCode: string): string | undefined {
|
|
56
|
+
return LANGUAGE_MAP[langCode];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isEnglishVariant(langCode: string): boolean {
|
|
60
|
+
const targetLang = getTargetLanguage(langCode);
|
|
61
|
+
return targetLang === "en";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getLanguageDisplayName(code: string): string {
|
|
65
|
+
return LANGUAGE_NAMES[code] || code;
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Hooks
|
|
3
|
+
* @description Exports all React hooks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { useTranslation } from "./useTranslation.hook";
|
|
7
|
+
export { useBatchTranslation } from "./useBatchTranslation.hook";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
UseTranslationOptions,
|
|
11
|
+
UseTranslationReturn,
|
|
12
|
+
} from "./useTranslation.hook";
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
UseBatchTranslationOptions,
|
|
16
|
+
UseBatchTranslationReturn,
|
|
17
|
+
} from "./useBatchTranslation.hook";
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useBatchTranslation Hook
|
|
3
|
+
* @description React hook for batch translation of multiple texts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
import type {
|
|
8
|
+
TranslationRequest,
|
|
9
|
+
TranslationStats,
|
|
10
|
+
} from "../../domain";
|
|
11
|
+
import { googleTranslateService } from "../../infrastructure/services";
|
|
12
|
+
|
|
13
|
+
export interface UseBatchTranslationOptions {
|
|
14
|
+
readonly onSuccess?: (stats: TranslationStats) => void;
|
|
15
|
+
readonly onError?: (error: Error) => void;
|
|
16
|
+
readonly onProgress?: (current: number, total: number) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseBatchTranslationReturn {
|
|
20
|
+
readonly translateBatch: (
|
|
21
|
+
requests: TranslationRequest[]
|
|
22
|
+
) => Promise<TranslationStats>;
|
|
23
|
+
readonly translateObject: (
|
|
24
|
+
sourceObject: Record<string, unknown>,
|
|
25
|
+
targetObject: Record<string, unknown>,
|
|
26
|
+
targetLanguage: string
|
|
27
|
+
) => Promise<TranslationStats>;
|
|
28
|
+
readonly isLoading: boolean;
|
|
29
|
+
readonly progress: number;
|
|
30
|
+
readonly total: number;
|
|
31
|
+
readonly error: Error | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useBatchTranslation(
|
|
35
|
+
options?: UseBatchTranslationOptions
|
|
36
|
+
): UseBatchTranslationReturn {
|
|
37
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
38
|
+
const [error, setError] = useState<Error | null>(null);
|
|
39
|
+
const [progress, setProgress] = useState(0);
|
|
40
|
+
const [total, setTotal] = useState(0);
|
|
41
|
+
|
|
42
|
+
const translateBatch = useCallback(
|
|
43
|
+
async (requests: TranslationRequest[]): Promise<TranslationStats> => {
|
|
44
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
45
|
+
return {
|
|
46
|
+
totalCount: 0,
|
|
47
|
+
successCount: 0,
|
|
48
|
+
failureCount: 0,
|
|
49
|
+
skippedCount: 0,
|
|
50
|
+
translatedKeys: [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setIsLoading(true);
|
|
55
|
+
setError(null);
|
|
56
|
+
setTotal(requests.length);
|
|
57
|
+
setProgress(0);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const stats = await googleTranslateService.translateBatch(requests);
|
|
61
|
+
|
|
62
|
+
setProgress(requests.length);
|
|
63
|
+
|
|
64
|
+
if (stats.failureCount > 0) {
|
|
65
|
+
const error = new Error(
|
|
66
|
+
`${stats.failureCount} of ${stats.totalCount} translations failed`
|
|
67
|
+
);
|
|
68
|
+
setError(error);
|
|
69
|
+
options?.onError?.(error);
|
|
70
|
+
} else {
|
|
71
|
+
options?.onSuccess?.(stats);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return stats;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const error =
|
|
77
|
+
err instanceof Error ? err : new Error("Unknown error occurred");
|
|
78
|
+
setError(error);
|
|
79
|
+
options?.onError?.(error);
|
|
80
|
+
throw error;
|
|
81
|
+
} finally {
|
|
82
|
+
setIsLoading(false);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
[options]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const translateObject = useCallback(
|
|
89
|
+
async (
|
|
90
|
+
sourceObject: Record<string, unknown>,
|
|
91
|
+
targetObject: Record<string, unknown>,
|
|
92
|
+
targetLanguage: string
|
|
93
|
+
): Promise<TranslationStats> => {
|
|
94
|
+
if (!sourceObject || typeof sourceObject !== "object") {
|
|
95
|
+
throw new Error("Source object is invalid");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!targetObject || typeof targetObject !== "object") {
|
|
99
|
+
throw new Error("Target object is invalid");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
103
|
+
throw new Error("Target language is required");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
setIsLoading(true);
|
|
107
|
+
setError(null);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const stats: TranslationStats = {
|
|
111
|
+
totalCount: 0,
|
|
112
|
+
successCount: 0,
|
|
113
|
+
failureCount: 0,
|
|
114
|
+
skippedCount: 0,
|
|
115
|
+
translatedKeys: [],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
await googleTranslateService.translateObject(
|
|
119
|
+
sourceObject,
|
|
120
|
+
targetObject,
|
|
121
|
+
targetLanguage,
|
|
122
|
+
"",
|
|
123
|
+
stats
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
setTotal(stats.totalCount);
|
|
127
|
+
setProgress(stats.totalCount);
|
|
128
|
+
|
|
129
|
+
if (stats.failureCount > 0) {
|
|
130
|
+
const error = new Error(
|
|
131
|
+
`${stats.failureCount} of ${stats.totalCount} translations failed`
|
|
132
|
+
);
|
|
133
|
+
setError(error);
|
|
134
|
+
options?.onError?.(error);
|
|
135
|
+
} else {
|
|
136
|
+
options?.onSuccess?.(stats);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return stats;
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const error =
|
|
142
|
+
err instanceof Error ? err : new Error("Unknown error occurred");
|
|
143
|
+
setError(error);
|
|
144
|
+
options?.onError?.(error);
|
|
145
|
+
throw error;
|
|
146
|
+
} finally {
|
|
147
|
+
setIsLoading(false);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
[options]
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
translateBatch,
|
|
155
|
+
translateObject,
|
|
156
|
+
isLoading,
|
|
157
|
+
progress,
|
|
158
|
+
total,
|
|
159
|
+
error,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTranslation Hook
|
|
3
|
+
* @description React hook for single text translation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useState } from "react";
|
|
7
|
+
import type { TranslationRequest, TranslationResponse } from "../../domain";
|
|
8
|
+
import { googleTranslateService } from "../../infrastructure/services";
|
|
9
|
+
|
|
10
|
+
export interface UseTranslationOptions {
|
|
11
|
+
readonly onSuccess?: (result: TranslationResponse) => void;
|
|
12
|
+
readonly onError?: (error: Error) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseTranslationReturn {
|
|
16
|
+
readonly translate: (text: string, targetLanguage: string) => Promise<TranslationResponse>;
|
|
17
|
+
readonly isLoading: boolean;
|
|
18
|
+
readonly error: Error | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useTranslation(
|
|
22
|
+
options?: UseTranslationOptions
|
|
23
|
+
): UseTranslationReturn {
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [error, setError] = useState<Error | null>(null);
|
|
26
|
+
|
|
27
|
+
const translate = useCallback(
|
|
28
|
+
async (text: string, targetLanguage: string): Promise<TranslationResponse> => {
|
|
29
|
+
if (!text || text.trim().length === 0) {
|
|
30
|
+
return {
|
|
31
|
+
originalText: text,
|
|
32
|
+
translatedText: text,
|
|
33
|
+
sourceLanguage: "en",
|
|
34
|
+
targetLanguage,
|
|
35
|
+
success: false,
|
|
36
|
+
error: "Text is empty",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
originalText: text,
|
|
43
|
+
translatedText: text,
|
|
44
|
+
sourceLanguage: "en",
|
|
45
|
+
targetLanguage,
|
|
46
|
+
success: false,
|
|
47
|
+
error: "Target language is empty",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setIsLoading(true);
|
|
52
|
+
setError(null);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const request: TranslationRequest = {
|
|
56
|
+
text,
|
|
57
|
+
targetLanguage,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const result = await googleTranslateService.translate(request);
|
|
61
|
+
|
|
62
|
+
if (result.success) {
|
|
63
|
+
options?.onSuccess?.(result);
|
|
64
|
+
} else {
|
|
65
|
+
const error = new Error(result.error || "Translation failed");
|
|
66
|
+
setError(error);
|
|
67
|
+
options?.onError?.(error);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const error =
|
|
73
|
+
err instanceof Error ? err : new Error("Unknown error occurred");
|
|
74
|
+
setError(error);
|
|
75
|
+
options?.onError?.(error);
|
|
76
|
+
throw error;
|
|
77
|
+
} finally {
|
|
78
|
+
setIsLoading(false);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[options]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
translate,
|
|
86
|
+
isLoading,
|
|
87
|
+
error,
|
|
88
|
+
};
|
|
89
|
+
}
|