Language switching is essential for making your app globally accessible.
Switching Languages Using JSON Files
The advantage of this method is that all language files are stored in JSON format, which can be downloaded from a server. This reduces the app's initial download size from the app store.
This approach is commonly used in mobile apps. However, it is not the method used in this particular website. I will introduce a different language-switching approach for Flutter web in the next section.
Create JSON Templates
See how I set up JSON files in the image. I have four languages: English, Japanese, Korean, and Vietnamese.
Here is the content inside the en.json file:
{
"welcomeBack": "Welcome Back",
"welcomeBackNameApp": "Welcome @nameUser Back @nameApp"
}
Here is the content inside the vi.json file:
{
"welcomeBack": "Chào mừng trở lại",
"welcomeBackNameApp": "Chào mừng @nameUser trở lại @nameApp"
}
Similarly, create JSON files for other languages.
You can see that we use the keys of the elements in the JSON file to identify the corresponding language.
First, we will customize the shared_preferences library a bit. This is how I separate libraries from my main project.
The advantage of this approach is that it makes it easy to replace libraries in the future if they are no longer supported and minimizes the complexity of integrating the library into the project.
I have explained this in my article about package management
I will omit unnecessary functions and keep only the main ones: setup, read, and write.
import 'dart:convert' as convert;
import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
class MyPrefs {
/*
Learn more: https://pub.dev/packages/shared_preferences
How to use?
Step 1: Define
MyPrefs myPrefs = MyPrefs();
Step 2: Setup on init
myPrefs.setUp();
Step 3: E.g use
myPrefs.write(constSaveLocale, locale.toString());
*/
SharedPreferences? prefs;
Future<void> setUp() async {
prefs = await SharedPreferences.getInstance();
}
Future<bool> tryBool(Future<bool> future) async {
try {
return future;
} catch (error) {
debugPrint(error.toString());
return Future(() => false);
}
}
Future<bool> write(String key, dynamic value) async {
final SharedPreferences prefs = this.prefs ??= await SharedPreferences.getInstance();
if (value.runtimeType == String) {
return tryBool(prefs.setString(key, value as String));
} else if (value.runtimeType == int) {
return tryBool(prefs.setInt(key, value as int));
} else if (value.runtimeType == bool) {
return tryBool(prefs.setBool(key, value as bool));
} else if (value.runtimeType == double) {
return tryBool(prefs.setDouble(key, value as double));
} else if (value.runtimeType == List<String>) {
return tryBool(prefs.setStringList(key, value as List<String>));
} else if (value.runtimeType.toString() == "_Map<String, dynamic>") {
return tryBool(prefs.setString(key, convert.jsonEncode(value as Map<String, dynamic>)));
} else if (value.runtimeType == DateTime) {
return tryBool(prefs.setString(key, value.toString()));
} else {
return Future(() => false);
}
}
/// Đảm bảo rằng đã chạy setUp()
Future<T?> read<T>(String key) async {
try {
final SharedPreferences prefs = this.prefs!;
if (T == String) {
return prefs.getString(key) as T;
} else if (T == int) {
return prefs.getInt(key) as T;
} else if (T == bool) {
return prefs.getBool(key) as T;
} else if (T == double) {
return prefs.getDouble(key) as T;
} else if (T == List<String>) {
return prefs.getStringList(key) as T;
} else if (T == Map<String, dynamic>) {
return prefs.getString(key) == null ? null : convert.jsonDecode(prefs.getString(key)!) as T;
} else if (T == DateTime) {
return prefs.getString(key) == null ? null : DateTime.parse(prefs.getString(key)!) as T;
} else {
return null;
}
} catch (error) {
return null;
}
}
}
After setting up the functions to store the current language in the device's memory, we move on to the setup step to read and retrieve the current language from the JSON file.
Assuming that all your language JSON files are stored in the "assets/i18n/" directory and you have declared them in pubspec.yaml.
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'shared_preference.dart';
const String constSaveLocale = 'constSaveLocaleInPackageCureStorage';
class MyLang {
late Locale locale;
late List<Locale> listLocale;
String pathInAssets = "assets/i18n/";
static Map<String, String>? localizedStrings;
MyPrefs myStorage = MyPrefs();
Future<void> setUp({required List<Locale> listLocale, String? path}) async {
this.listLocale = listLocale;
await myStorage.setUp();
locale = await loadLocal();
if (path != null) pathInAssets = path;
await loadFileJson();
}
/// Retrieve the stored locale from memory
Future<Locale> loadLocal() async {
final getLocale = await myStorage.read<String>(constSaveLocale);
if (getLocale != null) {
try {
return listLocale.firstWhere((element) => element.toString() == getLocale);
} catch (e) {
return locale;
}
} else {
return listLocale.first;
}
}
/// Save the locale to memory
void saveLocal({Locale? locale}) {
if (locale == null) return;
myStorage.write(constSaveLocale, locale.toString());
}
Future<bool> loadFileJson({Locale? locale}) async {
this.locale = locale ?? this.locale;
Intl.defaultLocale = this.locale.languageCode;
saveLocal(locale: locale);
final String jsonString = await rootBundle.loadString('$pathInAssets${this.locale.languageCode}${this.locale.countryCode != null ? ("-${this.locale.countryCode}") : ""}.json');
final Map<String, dynamic> jsonMap = json.decode(jsonString) as Map<String, dynamic>;
localizedStrings = jsonMap.map((key, value) {
return MapEntry(key, value.toString());
});
await WidgetsFlutterBinding.ensureInitialized().performReassemble();
return true;
}
static String translate(String key, {Map<String, String> params = const {}}) {
if (localizedStrings == null) return key;
var trans = localizedStrings![key];
if (trans == null || trans == "--") return key;
if (params.isNotEmpty) {
params.forEach((key, value) {
trans = trans!.replaceAll('@$key', value);
});
}
return trans!;
}
}
extension MyLocaleHelper on Locale {
bool get isEnglish => Locale(languageCode) == const Locale("en");
bool get isVietnamese => Locale(languageCode) == const Locale("vi");
bool get isKorean => Locale(languageCode) == const Locale("ko");
bool get isJapanese => Locale(languageCode) == const Locale("ja");
}
Initialize a global variable in the project:
MyLang myLang = MyLang();
Next, set up the list of languages that will be displayed in the application:
This approach is ideal for supporting multiple languages. It offers flexibility since translations can be fetched from a JSON file on a server. However, it comes with the trade-off of additional loading time.
Simple Language Switching with Inline Data
This method avoids loading language files from JSON, making it ideal for web applications. Let's get started:
We will store all language translations in a Dart file as a Map, like this:
const en = {
"success": "Success",
};
Similarly, we create maps for other languages.
Change the language of built-in Flutter dialogs, which default to English.
Format dates, times, and currency according to the selected language.
Idea
The approach is simple:
Store all translations in a single Map.
Retrieve a word or phrase by its key whenever needed.
class OurInterpreter {
OurInterpreter._();
/*
Function to set up the locale for the entire application and packages, always run this function when changing the language.
*/
static void setupDefaultLocale(String locale) {
Intl.defaultLocale = locale;
return;
}
static String get defaultLocale => Intl.shortLocale(Intl.defaultLocale ?? 'en');
static const _localizedValues = <String, Map<String, String>>{
'en': en,
'vi': vi,
'ja': ja,
'ko': ko,
'hi': hi,
'zh': zh,
};
static String _translate(
String key,
) {
try {
return _localizedValues[_localizedValues.containsKey(defaultLocale) ? defaultLocale : _localizedValues.keys.first]![key]!;
} catch (e) {
return key;
}
}
static String get success => _translate('success');
}
Use this code snippet to change the language to English:
OurInterpreter.setupDefaultLocale("en");
Usage:
Text(OurInterpreter.success),
This method is much simpler than the first one, but it is only suitable for applications with static data.
Step 2: Translate JSON files into multiple languages
Open the terminal with Admin privileges.
Run the following command:
npm i -g @parvineyvazov/json-translator
If you encounter the error zsh: command not found: jsontt, fix it by running:
alias jsontt="`npm config get prefix`/bin/jsontt"
On Windows, grant permission to run PowerShell scripts:
Set-ExecutionPolicy RemoteSigned
How to Use?
Step 1: Run the translation command
The translated JSON file will be saved in the same directory as the original file.
Example commands:
jsontt /Users/your_user/Desktop/flutter_project/assets/i18n/_parent.json
jsontt /Users/your_user/Desktop/flutter_project/assets/i18n/_parent.json --module google --from ko --to en ko vi ja
jsontt /Users/your_user/Desktop/flutter_project/assets/i18n/_parent.json --module bing --from ko --to en ko vi ja
jsontt /Users/your_user/Desktop/flutter_project/assets/i18n/_parent.json --module bing --to ko vi ja
Usage Tips:
Use Ctrl + Shift + C to copy the folder path.
Press Space to select the target language.
While this method helps automate translation, manual adjustments are still necessary, especially for technical terms, as machine translations may not always be accurate.
Using ChatGPT for Bulk Language Translation
The emergence of ChatGPT has significantly reduced the workload of tedious tasks, allowing us to engage with the world more effectively through language.
In the past, there were concerns about the accuracy of Google Translate. However, ChatGPT now provides much more natural and comprehensible translations, especially for technical documents.
How to Use ChatGPT for Bulk Translation
Here’s the command I frequently use to have ChatGPT translate multiple terms at once. This method is ideal for solo developers working on their projects:
Translate the following words into English, Japanese, Korean, and Vietnamese in JSON format, separating them by language. The JSON keys should be in English (translated from the original words) and written in camelCase.
Then, list out the JSON keys in the following code format:
static String get payApp => MyInterpreter.translate('payApp');
After receiving the translated JSON data and code snippet, simply copy and paste them into your JSON files and the OurInterpreter class.
Generating Code from JSON or Map to Reduce OurInterpreter Declaration Time
The idea is simple: I asked ChatGPT-4o to generate a Dart function that reads values from a JSON file and automatically creates an OurInterpreter class.
After receiving the generated code, I made a few adjustments to ensure accuracy and compliance with my preferred structure.
import 'dart:convert';
import 'dart:io';
String inputFilePath = '';
String outputFilePath = '';
/// STEP 1 | input file .json - output file interpreter.dart
// WINDOW PATH use r"PATH" instead for "PATH"
// Learn more https://dart.dev/language/built-in-types#:~:text=You%20can%20create%20a,gets%20special%20treatment.%27%3B
const defaultInputFilePath = '/Users/ducmng12/Desktop/flutter_project/FlutterPhotoBooth/assets/i18n/en.json';
const defaultOutputFilePath = '/Users/ducmng12/Desktop/flutter_project/FlutterPhotoBooth/lib/helper/languages/interpreter.dart';
/// STEP 2 | run
void main() async {
print('? Enter the input file path or press Enter to use default:');
String? input = stdin.readLineSync();
if (input != null && input.isNotEmpty) {
inputFilePath = input.trim();
} else {
inputFilePath = defaultInputFilePath;
print('Using default input file path.');
}
print('Enter the output file path or press Enter to use default:');
input = stdin.readLineSync();
if (input != null && input.isNotEmpty) {
outputFilePath = input.trim();
} else {
outputFilePath = defaultOutputFilePath;
print('Using default output file path.');
}
final file = File(inputFilePath);
final contents = await file.readAsString();
final Map<String, dynamic> jsonData = jsonDecode(contents) as Map<String, dynamic>;
jsonData.forEach((key, value) {
print(" static String get $key => MyInterpreter.translate('$key');");
});
await writeToFile(outputFilePath, jsonData);
}
Future<void> writeToFile(String filePath, Map<String, dynamic> data) async {
final file = File(filePath);
final sink = file.openWrite();
// Ghi nội dung vào tệp Dart
sink.writeln("// GENERATED CODE - CAREFULLY MODIFY BY HAND\n");
sink.writeln("import 'package:storage_core/src.dart';\n");
sink.writeln("/* **************************************************************************");
sink.writeln("RUN on project terminal");
sink.writeln("dart run packages/gen_core/lib/src/gen_interpreter.dart");
sink.writeln("************************************************************************** */");
sink.writeln('class OurLang extends MyLang {\n');
data.forEach((key, value) {
final List<String> split = value.toString().split(" ");
if (split.myItemContain("@")) {
final List<String> splitParam = split.where((e) => e.contains("@")).toList();
String a = "";
String b = "";
for (final e in splitParam) {
a += "String ${e.substring(1, e.length)},";
b += "'${e.substring(1, e.length)}': ${e.substring(1, e.length)},";
}
sink.writeln(" static String? $key($a) => MyLang.translate('$key', params: {$b});");
} else {
sink.writeln(" static String get $key => MyLang.translate('$key');");
}
});
sink.writeln('\n}');
await sink.close();
}
extension MyListStringHelper on List<String> {
/// Item list string contain context
bool myItemContain(String context) {
for (final element in this) {
if (element.contains(context)) return true;
}
return false;
}
/// context contain item in list
bool myContextContainItem(String context) {
for (final element in this) {
if (context.contains(element)) return true;
}
return false;
}
}
Before going into details, I will explain my idea:
I will read the content from the JSON file, then use the shared_preferences library to save the read content into the device's memory.
When using it, we only need to retrieve it from memory. When switching languages, we repeat this process from the beginning.
Next, we use the intl package () as the main localization library for the app. This library allows us to:
This is the first tool I used to automate language translation, saving a lot of time and effort.
You can refer to the documentation here: .
Refer to the official documentation: .
Below is the function I developed, which you can use as a reference. The key to leveraging AI effectively is to create tools that automate everything for you.
If you want to skip the complexity and use it quickly, check out my package on pub.dev: