vite-plugin-shipi18n 1.0.0

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 (3) hide show
  1. package/README.md +312 -0
  2. package/package.json +35 -0
  3. package/src/index.js +189 -0
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # vite-plugin-shipi18n
2
+
3
+ > Automatic i18n translation at build time for Vite projects
4
+
5
+ Translate your locale files automatically during the build process using [Shipi18n](https://shipi18n.com) API. No manual copy-pasting, no external tools - just add the plugin and run `npm run build`.
6
+
7
+ ## Features
8
+
9
+ - ⚔ **Zero-Config** - Works out of the box with sensible defaults
10
+ - šŸš€ **Build-Time Translation** - Translations happen during Vite build, not at runtime
11
+ - šŸ’¾ **Smart Caching** - Only translates changed content (hash-based caching)
12
+ - šŸŒ **Multi-Language Support** - Translate to multiple languages in one build
13
+ - šŸ”§ **Preserves Placeholders** - Keeps `{{name}}`, `{count}`, `%s`, etc. intact
14
+ - šŸ“¦ **Framework Agnostic** - Works with any i18n library (react-i18next, vue-i18n, etc.)
15
+ - šŸŽÆ **Production Ready** - Battle-tested caching and error handling
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install vite-plugin-shipi18n --save-dev
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### 1. Get Your API Key
26
+
27
+ Sign up at [shipi18n.com](https://shipi18n.com) to get your free API key (takes 30 seconds, no credit card required).
28
+
29
+ Free tier includes:
30
+ - 60 requests/minute
31
+ - 10,000 characters/month
32
+ - Perfect for small to medium projects
33
+
34
+ ### 2. Configure the Plugin
35
+
36
+ Add to your `vite.config.js`:
37
+
38
+ ```javascript
39
+ import { defineConfig } from 'vite'
40
+ import shipi18n from 'vite-plugin-shipi18n'
41
+
42
+ export default defineConfig({
43
+ plugins: [
44
+ shipi18n({
45
+ apiKey: process.env.VITE_SHIPI18N_API_KEY,
46
+ targetLanguages: ['es', 'fr', 'de', 'ja'],
47
+ sourceDir: 'public/locales/en',
48
+ outputDir: 'public/locales'
49
+ })
50
+ ]
51
+ })
52
+ ```
53
+
54
+ ### 3. Create Your Source Locale File
55
+
56
+ Create `public/locales/en/translation.json`:
57
+
58
+ ```json
59
+ {
60
+ "welcome": "Welcome to our app!",
61
+ "greeting": "Hello, {{name}}!",
62
+ "message_count": "You have {{count}} messages"
63
+ }
64
+ ```
65
+
66
+ ### 4. Build Your Project
67
+
68
+ ```bash
69
+ npm run build
70
+ ```
71
+
72
+ The plugin will automatically:
73
+ 1. Read your English locale files
74
+ 2. Translate them to Spanish, French, German, and Japanese
75
+ 3. Save translated files to `public/locales/es/`, `public/locales/fr/`, etc.
76
+ 4. Cache results for faster subsequent builds
77
+
78
+ ## Configuration
79
+
80
+ ### Plugin Options
81
+
82
+ | Option | Type | Default | Description |
83
+ |--------|------|---------|-------------|
84
+ | `apiKey` | `string` | **required** | Your Shipi18n API key |
85
+ | `targetLanguages` | `string[]` | **required** | Array of target language codes (e.g., `['es', 'fr', 'de']`) |
86
+ | `sourceDir` | `string` | `'public/locales/en'` | Directory containing source locale files |
87
+ | `outputDir` | `string` | `'public/locales'` | Directory where translated files will be saved |
88
+ | `sourceLanguage` | `string` | `'en'` | Source language code |
89
+ | `apiUrl` | `string` | Shipi18n production URL | Custom API URL (for self-hosted instances) |
90
+ | `cache` | `boolean` | `true` | Enable smart caching |
91
+ | `cacheDir` | `string` | `'node_modules/.cache/vite-plugin-shipi18n'` | Cache directory path |
92
+
93
+ ### Full Configuration Example
94
+
95
+ ```javascript
96
+ import { defineConfig } from 'vite'
97
+ import shipi18n from 'vite-plugin-shipi18n'
98
+
99
+ export default defineConfig({
100
+ plugins: [
101
+ shipi18n({
102
+ // Required
103
+ apiKey: process.env.VITE_SHIPI18N_API_KEY,
104
+ targetLanguages: ['es', 'fr', 'de', 'ja', 'zh', 'pt', 'ru', 'ar'],
105
+
106
+ // Optional (with defaults shown)
107
+ sourceDir: 'public/locales/en',
108
+ outputDir: 'public/locales',
109
+ sourceLanguage: 'en',
110
+ cache: true,
111
+ cacheDir: 'node_modules/.cache/vite-plugin-shipi18n'
112
+ })
113
+ ]
114
+ })
115
+ ```
116
+
117
+ ## How It Works
118
+
119
+ 1. **Build Hook**: Plugin runs during Vite's build process (`buildStart` hook)
120
+ 2. **File Discovery**: Scans `sourceDir` for all `.json` files
121
+ 3. **Cache Check**: Checks if file content has changed (using MD5 hash)
122
+ 4. **API Call**: If changed, calls Shipi18n API to translate
123
+ 5. **File Generation**: Writes translated JSON files to `outputDir/<lang>/<filename>.json`
124
+ 6. **Cache Update**: Saves translation results for future builds
125
+
126
+ ### Caching Strategy
127
+
128
+ The plugin uses content-based hashing to determine if translation is needed:
129
+
130
+ ```
131
+ Hash = MD5(file_content + target_languages)
132
+ ```
133
+
134
+ - āœ… **Changed content** = Re-translate
135
+ - āœ… **Added languages** = Re-translate
136
+ - ā­ļø **Unchanged** = Use cache
137
+
138
+ Cache files are stored in `node_modules/.cache/vite-plugin-shipi18n/` and are safe to delete.
139
+
140
+ ## Usage with i18n Libraries
141
+
142
+ ### React + react-i18next
143
+
144
+ ```javascript
145
+ // src/i18n.js
146
+ import i18n from 'i18next'
147
+ import { initReactI18next } from 'react-i18next'
148
+
149
+ i18n
150
+ .use(initReactI18next)
151
+ .init({
152
+ fallbackLng: 'en',
153
+ lng: 'en',
154
+ resources: {} // Resources loaded from public/locales
155
+ })
156
+
157
+ // Load translations dynamically
158
+ const loadLanguage = async (lng) => {
159
+ const translation = await fetch(`/locales/${lng}/translation.json`)
160
+ const data = await translation.json()
161
+ i18n.addResourceBundle(lng, 'translation', data)
162
+ }
163
+
164
+ export default i18n
165
+ ```
166
+
167
+ ### Vue + vue-i18n
168
+
169
+ ```javascript
170
+ // src/i18n.js
171
+ import { createI18n } from 'vue-i18n'
172
+
173
+ const i18n = createI18n({
174
+ locale: 'en',
175
+ fallbackLocale: 'en',
176
+ messages: {} // Messages loaded from public/locales
177
+ })
178
+
179
+ export default i18n
180
+ ```
181
+
182
+ ### Next.js + next-i18next
183
+
184
+ ```javascript
185
+ // next-i18next.config.js
186
+ module.exports = {
187
+ i18n: {
188
+ locales: ['en', 'es', 'fr', 'de'],
189
+ defaultLocale: 'en',
190
+ },
191
+ localePath: './public/locales'
192
+ }
193
+ ```
194
+
195
+ ## Supported Languages
196
+
197
+ The plugin supports **100+ languages** via Google Cloud Translation API, including:
198
+
199
+ - **es** - Spanish
200
+ - **fr** - French
201
+ - **de** - German
202
+ - **ja** - Japanese
203
+ - **zh** - Chinese (Simplified)
204
+ - **zh-TW** - Chinese (Traditional)
205
+ - **pt** - Portuguese
206
+ - **ru** - Russian
207
+ - **ar** - Arabic
208
+ - **hi** - Hindi
209
+ - **ko** - Korean
210
+ - **it** - Italian
211
+ - **nl** - Dutch
212
+ - **pl** - Polish
213
+ - **tr** - Turkish
214
+ - **vi** - Vietnamese
215
+ - **th** - Thai
216
+ - **id** - Indonesian
217
+ - **sv** - Swedish
218
+ - **and 85+ more...**
219
+
220
+ See the [shipi18n-react-example](https://github.com/shipi18n/shipi18n-react-example/blob/main/src/constants/languages.js) repository for the complete list of 100+ supported language codes.
221
+
222
+ ## Example Project
223
+
224
+ See the [example/](./example) directory for a complete working example with React + react-i18next.
225
+
226
+ ### Run the Example
227
+
228
+ ```bash
229
+ cd example
230
+ npm install
231
+ cp .env.example .env
232
+ # Add your API key to .env
233
+ npm run build # Generates translations
234
+ npm run dev # Run the app
235
+ ```
236
+
237
+ ## FAQ
238
+
239
+ ### Does this work with any i18n library?
240
+
241
+ Yes! The plugin simply generates translated JSON files. It works with:
242
+ - react-i18next
243
+ - vue-i18n
244
+ - next-i18next
245
+ - Any library that loads locale files from a directory
246
+
247
+ ### When does translation happen?
248
+
249
+ During the Vite build process (`vite build`), **not** at runtime. This means:
250
+ - āœ… Zero runtime overhead
251
+ - āœ… All translations are static files
252
+ - āœ… Works offline after build
253
+ - āœ… No API calls from users' browsers
254
+
255
+ ### What if I change my English text?
256
+
257
+ The plugin detects changes and re-translates automatically. Caching ensures only changed files are re-translated.
258
+
259
+ ### Can I translate from a language other than English?
260
+
261
+ Yes! Use the `sourceLanguage` option:
262
+
263
+ ```javascript
264
+ shipi18n({
265
+ sourceLanguage: 'es', // Translate from Spanish
266
+ targetLanguages: ['en', 'fr', 'de']
267
+ })
268
+ ```
269
+
270
+ ### What about placeholders?
271
+
272
+ Placeholders are automatically preserved:
273
+ - `{{name}}` → stays as `{{name}}`
274
+ - `{count}` → stays as `{count}}`
275
+ - `%s` → stays as `%s`
276
+ - `<0>` → stays as `<0>`
277
+
278
+ ### Can I use this in CI/CD?
279
+
280
+ Yes! Set your API key as an environment variable in your CI/CD pipeline:
281
+
282
+ ```bash
283
+ # GitHub Actions
284
+ VITE_SHIPI18N_API_KEY=${{ secrets.SHIPI18N_API_KEY }}
285
+
286
+ # GitLab CI
287
+ VITE_SHIPI18N_API_KEY: $SHIPI18N_API_KEY
288
+
289
+ # Environment variable
290
+ export VITE_SHIPI18N_API_KEY=sk_live_...
291
+ ```
292
+
293
+ ### Does it slow down my build?
294
+
295
+ First build: Yes, translations take time (depends on file size and number of languages).
296
+ Subsequent builds: No, cached translations are used instantly.
297
+
298
+ **Tip**: Cache `node_modules/.cache/vite-plugin-shipi18n/` in your CI/CD for faster builds.
299
+
300
+ ## License
301
+
302
+ MIT
303
+
304
+ ## Support
305
+
306
+ - [Documentation](https://shipi18n.com/docs)
307
+ - [GitHub Issues](https://github.com/Shipi18n/vite-plugin-shipi18n/issues)
308
+ - [Discord Community](https://discord.gg/shipi18n)
309
+
310
+ ---
311
+
312
+ Built with [Shipi18n](https://shipi18n.com) - Smart translation API for developers
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "vite-plugin-shipi18n",
3
+ "version": "1.0.0",
4
+ "description": "Vite plugin for automatic i18n translation at build time using Shipi18n API",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "src/index.js",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
13
+ "test:watch": "NODE_OPTIONS='--experimental-vm-modules' jest --watch"
14
+ },
15
+ "keywords": [
16
+ "vite",
17
+ "vite-plugin",
18
+ "i18n",
19
+ "translation",
20
+ "localization",
21
+ "shipi18n"
22
+ ],
23
+ "author": "Shipi18n",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/Shipi18n/vite-plugin-shipi18n.git"
28
+ },
29
+ "peerDependencies": {
30
+ "vite": "^4.0.0 || ^5.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "jest": "^29.7.0"
34
+ }
35
+ }
package/src/index.js ADDED
@@ -0,0 +1,189 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import crypto from 'crypto'
4
+
5
+ /**
6
+ * Vite plugin for automatic i18n translation using Shipi18n API
7
+ *
8
+ * @param {Object} options - Plugin configuration
9
+ * @param {string} options.apiKey - Shipi18n API key (required)
10
+ * @param {string} options.apiUrl - Shipi18n API URL (optional, defaults to production)
11
+ * @param {string[]} options.targetLanguages - Languages to translate to (required)
12
+ * @param {string} options.sourceDir - Directory containing source locale files (default: 'public/locales/en')
13
+ * @param {string} options.outputDir - Directory to write translated files (default: 'public/locales')
14
+ * @param {string} options.sourceLanguage - Source language code (default: 'en')
15
+ * @param {boolean} options.cache - Enable caching (default: true)
16
+ * @param {string} options.cacheDir - Cache directory (default: 'node_modules/.cache/vite-plugin-shipi18n')
17
+ */
18
+ export default function shipi18nPlugin(options = {}) {
19
+ const {
20
+ apiKey,
21
+ apiUrl = 'https://x9527l3blg.execute-api.us-east-1.amazonaws.com',
22
+ targetLanguages = [],
23
+ sourceDir = 'public/locales/en',
24
+ outputDir = 'public/locales',
25
+ sourceLanguage = 'en',
26
+ cache = true,
27
+ cacheDir = 'node_modules/.cache/vite-plugin-shipi18n'
28
+ } = options
29
+
30
+ // Validation
31
+ if (!apiKey) {
32
+ throw new Error('vite-plugin-shipi18n: apiKey is required')
33
+ }
34
+
35
+ if (!targetLanguages || targetLanguages.length === 0) {
36
+ throw new Error('vite-plugin-shipi18n: targetLanguages is required')
37
+ }
38
+
39
+ let config
40
+
41
+ return {
42
+ name: 'vite-plugin-shipi18n',
43
+
44
+ configResolved(resolvedConfig) {
45
+ config = resolvedConfig
46
+ },
47
+
48
+ async buildStart() {
49
+ const root = config.root || process.cwd()
50
+ const sourcePath = path.resolve(root, sourceDir)
51
+ const outputPath = path.resolve(root, outputDir)
52
+ const cachePath = path.resolve(root, cacheDir)
53
+
54
+ console.log('\nšŸŒ Shipi18n: Starting translation process...')
55
+ console.log(` Source: ${sourceDir}`)
56
+ console.log(` Target languages: ${targetLanguages.join(', ')}`)
57
+
58
+ // Check if source directory exists
59
+ if (!fs.existsSync(sourcePath)) {
60
+ console.warn(`āš ļø Warning: Source directory not found: ${sourcePath}`)
61
+ return
62
+ }
63
+
64
+ // Create cache directory if needed
65
+ if (cache && !fs.existsSync(cachePath)) {
66
+ fs.mkdirSync(cachePath, { recursive: true })
67
+ }
68
+
69
+ // Find all JSON files in source directory
70
+ const sourceFiles = fs.readdirSync(sourcePath)
71
+ .filter(file => file.endsWith('.json'))
72
+
73
+ if (sourceFiles.length === 0) {
74
+ console.warn(`āš ļø Warning: No JSON files found in ${sourcePath}`)
75
+ return
76
+ }
77
+
78
+ console.log(` Found ${sourceFiles.length} source file(s)`)
79
+
80
+ // Process each file
81
+ for (const fileName of sourceFiles) {
82
+ const sourceFilePath = path.join(sourcePath, fileName)
83
+ const sourceContent = fs.readFileSync(sourceFilePath, 'utf-8')
84
+
85
+ let sourceJson
86
+ try {
87
+ sourceJson = JSON.parse(sourceContent)
88
+ } catch (error) {
89
+ console.error(`āŒ Error parsing ${fileName}: ${error.message}`)
90
+ continue
91
+ }
92
+
93
+ // Calculate hash for caching
94
+ const contentHash = crypto
95
+ .createHash('md5')
96
+ .update(sourceContent + targetLanguages.join(','))
97
+ .digest('hex')
98
+
99
+ const cacheFile = path.join(cachePath, `${fileName}.${contentHash}.json`)
100
+
101
+ // Check cache
102
+ let translations = null
103
+ if (cache && fs.existsSync(cacheFile)) {
104
+ console.log(` āœ“ ${fileName}: Using cached translations`)
105
+ try {
106
+ translations = JSON.parse(fs.readFileSync(cacheFile, 'utf-8'))
107
+ } catch (error) {
108
+ console.warn(` āš ļø Cache corrupted for ${fileName}, re-translating...`)
109
+ translations = null
110
+ }
111
+ }
112
+
113
+ // Translate if not cached
114
+ if (!translations) {
115
+ console.log(` ā³ ${fileName}: Translating to ${targetLanguages.length} language(s)...`)
116
+
117
+ try {
118
+ translations = await translateJSON({
119
+ apiKey,
120
+ apiUrl,
121
+ json: sourceJson,
122
+ sourceLanguage,
123
+ targetLanguages
124
+ })
125
+
126
+ // Save to cache
127
+ if (cache) {
128
+ fs.writeFileSync(cacheFile, JSON.stringify(translations, null, 2))
129
+ }
130
+
131
+ console.log(` āœ“ ${fileName}: Translation complete`)
132
+ } catch (error) {
133
+ console.error(` āŒ ${fileName}: Translation failed - ${error.message}`)
134
+ continue
135
+ }
136
+ }
137
+
138
+ // Write translated files
139
+ for (const langCode of targetLanguages) {
140
+ if (!translations[langCode]) {
141
+ console.warn(` āš ļø ${fileName}: No translation for ${langCode}`)
142
+ continue
143
+ }
144
+
145
+ const outputLangDir = path.join(outputPath, langCode)
146
+ if (!fs.existsSync(outputLangDir)) {
147
+ fs.mkdirSync(outputLangDir, { recursive: true })
148
+ }
149
+
150
+ const outputFile = path.join(outputLangDir, fileName)
151
+ fs.writeFileSync(
152
+ outputFile,
153
+ JSON.stringify(translations[langCode], null, 2)
154
+ )
155
+ }
156
+ }
157
+
158
+ console.log('āœ… Shipi18n: Translation complete!\n')
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Call Shipi18n API to translate JSON
165
+ */
166
+ async function translateJSON({ apiKey, apiUrl, json, sourceLanguage, targetLanguages }) {
167
+ const response = await fetch(`${apiUrl}/api/translate`, {
168
+ method: 'POST',
169
+ headers: {
170
+ 'Content-Type': 'application/json',
171
+ 'X-API-Key': apiKey
172
+ },
173
+ body: JSON.stringify({
174
+ inputMethod: 'json',
175
+ jsonInput: JSON.stringify(json),
176
+ sourceLanguage,
177
+ targetLanguages: JSON.stringify(targetLanguages),
178
+ preservePlaceholders: 'true'
179
+ })
180
+ })
181
+
182
+ if (!response.ok) {
183
+ const error = await response.json().catch(() => ({}))
184
+ throw new Error(error.message || `API request failed: ${response.status}`)
185
+ }
186
+
187
+ const data = await response.json()
188
+ return data.translations || {}
189
+ }