-
-
Notifications
You must be signed in to change notification settings - Fork 62
Expand file tree
/
Copy pathi18n.js
More file actions
executable file
·301 lines (251 loc) · 8.49 KB
/
i18n.js
File metadata and controls
executable file
·301 lines (251 loc) · 8.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
const fs = require('fs');
const path = require('path');
const { executeCommand } = require('../utils/commands');
const { log } = require('../utils/logger');
const { parseJsonFile } = require('../utils/config');
const {
updateBabelAliases,
writeBabelConfig,
} = require('../utils/babel-config');
async function setupI18n(projectPath, isExpo, pm) {
log.info('Installing i18next with TypeScript support...');
await executeCommand(
pm.add('i18next react-i18next react-native-mmkv react-native-nitro-modules')
);
if (isExpo) {
await executeCommand('npx expo install react-native-localize');
await executeCommand(
pm.addDev('babel-plugin-module-resolver babel-preset-expo @expo/config-plugins')
);
} else {
await executeCommand(pm.add('react-native-localize'));
await executeCommand(pm.addDev('babel-plugin-module-resolver'));
}
// Ensure src directory exists (for both Expo and React Native CLI)
const srcDir = path.join(projectPath, 'src');
if (!fs.existsSync(srcDir)) {
fs.mkdirSync(srcDir);
}
// Create i18n directory inside src
const i18nDir = path.join(srcDir, 'i18n');
if (!fs.existsSync(i18nDir)) {
fs.mkdirSync(i18nDir, { recursive: true });
}
// Create locales directory
const localesDir = path.join(i18nDir, 'locales');
if (!fs.existsSync(localesDir)) {
fs.mkdirSync(localesDir);
}
// Create translation files
const translations = {
en: {
common: {
english: 'English',
italian: 'Italian',
language: 'Language',
goTo: 'Go to',
goBack: 'Go Back',
},
HomeScreen: {
title: 'Home',
description:
'This is a boilerplate React Native application with navigation and i18n support.',
},
DetailsScreen: {
title: 'Details',
description: 'This is the details page of your application.',
},
},
it: {
common: {
english: 'Inglese',
italian: 'Italiano',
language: 'Lingua',
goTo: 'Vai a',
goBack: 'Indietro',
},
HomeScreen: {
title: 'Home',
description:
'Questo è un boilerplate React Native con navigazione e supporto i18n.',
},
DetailsScreen: {
title: 'Dettaglio',
description: 'Questa è la pagina dei dettagli della tua applicazione.',
},
},
};
fs.writeFileSync(
path.join(localesDir, 'en.json'),
JSON.stringify(translations.en, null, 2)
);
fs.writeFileSync(
path.join(localesDir, 'it.json'),
JSON.stringify(translations.it, null, 2)
);
// Create languageConfig.ts
const languageConfig = `// Import here your languages
import en from './locales/en.json';
import it from './locales/it.json';
// Set here you favourite default language
export const defaultLanguage = 'en';
// Export here your language files import
// Structure resources with namespace wrapping
export const languagesResources = {
en: {
common: en,
},
it: {
common: it,
},
};
`;
fs.writeFileSync(path.join(i18nDir, 'languageConfig.ts'), languageConfig);
// Create mmkv directory inside src (srcDir already defined above)
const mmkvDir = path.join(srcDir, 'mmkv');
if (!fs.existsSync(mmkvDir)) {
fs.mkdirSync(mmkvDir, { recursive: true });
}
// Create mmkv storage setup
const mmkvSetup = `import { createMMKV } from 'react-native-mmkv';
export const MMKVStorage = createMMKV({
id: 'user-starter-storage', // please change this one according to your project
});
`;
fs.writeFileSync(path.join(mmkvDir, 'index.ts'), mmkvSetup);
// Create languageDetector.ts
const languageDetector = `import { MMKVStorage } from '@mmkv';
import { findBestLanguageTag } from 'react-native-localize';
import { defaultLanguage, languagesResources } from './languageConfig';
const LOCALE_PERSISTENCE_KEY = 'app_locale';
const noop = (): void => {
// Do nothing
};
const RNLanguageDetector = {
type: 'languageDetector',
async: true,
detect: (cb: (detectedLocale: string) => void): void => {
try {
// Retrieve cached locale
const persistedLocale = MMKVStorage.getString(LOCALE_PERSISTENCE_KEY);
// If not found, detect from device
if (!persistedLocale) {
// Find best available language from the resource ones
const languageTags = Object.keys(languagesResources);
const detectedLocale = findBestLanguageTag(languageTags);
// Return detected locale or default language
return cb(detectedLocale?.languageTag ?? defaultLanguage);
}
cb(persistedLocale);
} catch {
console.warn('Failed to detect locale!');
console.warn('Will use defaultLanguage:', defaultLanguage);
cb(defaultLanguage);
}
},
init: noop,
cacheUserLanguage: (locale: string): void => {
MMKVStorage.set(LOCALE_PERSISTENCE_KEY, locale);
},
};
export default RNLanguageDetector;
`;
fs.writeFileSync(path.join(i18nDir, 'languageDetector.ts'), languageDetector);
// Create i18n.ts config
const i18nConfig = `import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import { Text } from 'react-native';
import { defaultLanguage, languagesResources } from './languageConfig';
import RNLanguageDetector from './languageDetector';
i18n
// @ts-ignore
.use(RNLanguageDetector)
.use(initReactI18next) // passes i18n down to react-i18next
.init({
// @ts-ignore
debug: process.env.NODE_ENV === 'development',
resources: languagesResources,
compatibilityJSON: 'v3',
// language to use if translations in user language are not available.
fallbackLng: defaultLanguage,
ns: ['common'],
defaultNS: 'common',
// Use dot notation for both namespaces and keys
keySeparator: '.',
nsSeparator: false, // Disable namespace separator to use dots for nested keys only
interpolation: {
escapeValue: false,
},
react: {
useSuspense: true,
defaultTransParent: Text,
transSupportBasicHtmlNodes: false,
},
});
export default i18n;
`;
fs.writeFileSync(path.join(i18nDir, 'i18n.ts'), i18nConfig);
// Create utils directory inside i18n
const i18nUtilsDir = path.join(i18nDir, 'utils');
if (!fs.existsSync(i18nUtilsDir)) {
fs.mkdirSync(i18nUtilsDir);
}
// Create i18n utils
const i18nUtils = `import i18n from '@i18n/i18n';
export const switchLocaleTo = (locale: string) => {
i18n.changeLanguage(locale);
};
`;
fs.writeFileSync(path.join(i18nUtilsDir, 'index.ts'), i18nUtils);
// Update tsconfig.json to include i18n-specific path aliases
const tsconfigPath = path.join(projectPath, 'tsconfig.json');
const tsconfig = parseJsonFile(tsconfigPath);
if (!tsconfig.compilerOptions) {
tsconfig.compilerOptions = {};
}
if (!tsconfig.compilerOptions.paths) {
tsconfig.compilerOptions.paths = {};
}
// Add i18n-specific path aliases
tsconfig.compilerOptions.paths['@mmkv'] = ['./src/mmkv'];
tsconfig.compilerOptions.paths['@i18n/*'] = ['./src/i18n/*'];
// Add general aliases if not already present
if (!tsconfig.compilerOptions.paths['@components/*']) {
tsconfig.compilerOptions.paths['@components/*'] = ['./src/components/*'];
}
if (!tsconfig.compilerOptions.paths['@screens/*']) {
tsconfig.compilerOptions.paths['@screens/*'] = ['./src/screens/*'];
}
// Add @navigation alias if react-navigation is used (navigation.d.ts exists)
if (fs.existsSync(path.join(projectPath, 'navigation.d.ts'))) {
tsconfig.compilerOptions.paths['@navigation'] = ['./navigation.d.ts'];
}
fs.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
// Update babel.config.js to include path aliases
const preset = isExpo
? 'babel-preset-expo'
: 'module:@react-native/babel-preset';
const aliases = {
'@mmkv': './src/mmkv',
'@i18n': './src/i18n',
'@components': './src/components',
'@screens': './src/screens',
};
// Add @navigation alias if react-navigation is used
if (fs.existsSync(path.join(projectPath, 'navigation.d.ts'))) {
aliases['@navigation'] = './navigation.d.ts';
}
const babelConfig = updateBabelAliases(projectPath, aliases, preset);
writeBabelConfig(projectPath, babelConfig);
log.success('i18next configured with MMKV storage and language detector');
log.success('Babel configured with module resolver for path aliases');
// Add reminder for Expo projects using MMKV
if (isExpo) {
log.warning(
'⚠️ IMPORTANT: Since this project uses react-native-mmkv, you need to run "npx expo prebuild" before starting the app to generate native code.'
);
}
}
module.exports = {
setupI18n,
};