refactor: major changes I18n

feature/issue-4/translation
Frédérik Benoist 2023-11-01 19:11:03 +01:00
parent 01e53128ee
commit aa88116123
20 changed files with 136 additions and 98 deletions

View File

@ -1,3 +0,0 @@
{
"text_description": "مرحبًا ، سيتغير هذا النص وفقًا للغة"
}

View File

@ -1,5 +1,14 @@
{ {
"text_description": "Hello, this text will change according to the language", "i18n_label_hello" : "Hello",
"i18n_hello" : "Hello", "i18n_take_pictures" : "Take pictures",
"i18n_take_pictures" : "Take pictures" "i18n_menu_settings" : "Settings",
"i18n_menu_about" : "About",
"i18n_menu_show_logs" : "Show logs",
"i18n_menu_check_version" : "Check version",
"i18n_menu_account" : "Account",
"i18n_label_sign_out" : "Sign Out",
"i18n_label_photo_quality" : "Photo quality",
"i18n_label_photo_resizing" : "Photo resizing",
"i18n_label_sound_photo" : "Play sound when taking photo",
"i18n_label_language" : "Language"
} }

View File

@ -1,4 +1,14 @@
{ {
"i18n_hello": "Bonjour", "i18n_label_hello": "Bonjour",
"i18n_take_pictures" : "Prendre des photos" "i18n_take_pictures" : "Prendre des photos",
"i18n_menu_settings" : "Paramètres",
"i18n_menu_about" : "A propos",
"i18n_menu_show_logs" : "Voir logs",
"i18n_menu_check_version" : "Vérifier version",
"i18n_menu_account" : "Profil",
"i18n_label_sign_out" : "Déconnecter",
"i18n_label_photo_quality" : "Qualité photo",
"i18n_label_photo_resizing" : "Redimensionnement photo",
"i18n_label_sound_photo" : "Jouer son quand prise photo",
"i18n_label_language" : "Langage"
} }

View File

@ -1,3 +0,0 @@
{
"text_description": "नमस्कार, यह पाठ भाषा के अनुसार बदल जाएगा"
}

View File

@ -1,3 +0,0 @@
{
"text_description": "Halo, teks ini akan berubah menurut bahasanya"
}

View File

@ -1,3 +0,0 @@
{
"text_description": "สวัสดีข้อความนี้จะเปลี่ยนไปตามภาษา"
}

View File

@ -1,3 +0,0 @@
{
"text_description": "Merhaba, bu yazı dile göre değişecek"
}

View File

@ -1,3 +0,0 @@
{
"text_description": "您好,该文字会根据语言而变化"
}

View File

@ -9,7 +9,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:mobdr/service/shared_prefs.dart';
class AppLocalizations { class AppLocalizations {
final Locale locale; final Locale locale;
@ -36,8 +36,9 @@ class AppLocalizations {
} }
// This method will be called from every widget which needs a localized text // This method will be called from every widget which needs a localized text
String? translate(String key) { String translate(String key) {
return _localizedStrings[key]; final translatedString = _localizedStrings[key];
return translatedString ?? 'I18N:' + key;
} }
} }
@ -47,24 +48,17 @@ class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
@override @override
bool isSupported(Locale locale) { bool isSupported(Locale locale) {
// Include all of your supported language codes here // Include all of your supported language codes here
return ['fr', 'en', 'id', 'ar', 'zh', 'hi', 'th', 'tk'] return ['fr', 'en'].contains(locale.languageCode);
.contains(locale.languageCode);
} }
@override @override
Future<AppLocalizations> load(Locale locale) async { Future<AppLocalizations> load(Locale locale) async {
final SharedPreferences _pref = await SharedPreferences.getInstance(); locale = SharedPrefs().language;
String? lCode = _pref.getString('lCode');
String? cCode = _pref.getString('cCode');
if (lCode == null || cCode == null) {
await _pref.setString('lCode', locale.languageCode);
await _pref.setString('cCode', locale.countryCode!);
} else {
locale = Locale(lCode, cCode);
}
AppLocalizations localizations = new AppLocalizations(locale); AppLocalizations localizations = new AppLocalizations(locale);
await localizations.load(); await localizations.load();
return localizations; return localizations;
} }

View File

@ -36,3 +36,12 @@ class SynchronizationEvent extends AppEvent {
@override @override
List<Object?> get props => [isRunning]; List<Object?> get props => [isRunning];
} }
class ChangeLocaleEvent extends AppEvent {
ChangeLocaleEvent(this.language);
final String language;
@override
List<Object?> get props => [language];
}

View File

@ -165,16 +165,7 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
}), }),
), ),
// below is used for language feature // below is used for language feature
supportedLocales: [ supportedLocales: [Locale('fr', 'FR'), Locale('en', 'US')],
Locale('fr', 'FR'),
Locale('en', 'US'),
Locale('id', 'ID'),
Locale('ar', 'DZ'),
Locale('zh', 'HK'),
Locale('hi', 'IN'),
Locale('th', 'TH'),
Locale('tk', 'TK'),
],
// These delegates make sure that the localization data for the proper language is loaded // These delegates make sure that the localization data for the proper language is loaded
localizationsDelegates: [ localizationsDelegates: [
AppLocalizationsDelegate(), AppLocalizationsDelegate(),

View File

@ -1,5 +1,6 @@
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:mobdr/main.dart'; import 'package:mobdr/main.dart';
import 'package:mobdr/service/shared_prefs.dart';
class VisitModel { class VisitModel {
late int id; late int id;
@ -31,7 +32,6 @@ class VisitModel {
static Future<List<VisitModel>> getTodayVisits() async { static Future<List<VisitModel>> getTodayVisits() async {
// Retrieve all today visits from the database // Retrieve all today visits from the database
final visits = await objectbox.getTodayVisit(); final visits = await objectbox.getTodayVisit();
// Map each retrieved visit to VisiteModel // Map each retrieved visit to VisiteModel
final visitModelList = visits final visitModelList = visits
.map((visite) => VisitModel( .map((visite) => VisitModel(
@ -41,7 +41,7 @@ class VisitModel {
id_visite: visite.id_visite, id_visite: visite.id_visite,
name: visite.id_etab.toString() + ' - ' + visite.title, name: visite.id_etab.toString() + ' - ' + visite.title,
photoCount: objectbox.getVisitPhotoTaken(visite.id_visite), photoCount: objectbox.getVisitPhotoTaken(visite.id_visite),
date: DateFormat('EEEE d MMMM HH:mm', 'fr_FR') date: DateFormat('EEEE d MMMM HH:mm', SharedPrefs().locale)
.format(visite.date_visite), .format(visite.date_visite),
image: visite.url_photo_principale, image: visite.url_photo_principale,
type_visite: visite.type_visite, type_visite: visite.type_visite,
@ -66,7 +66,7 @@ class VisitModel {
id_visite: visite.id_visite, id_visite: visite.id_visite,
name: visite.id_etab.toString() + ' - ' + visite.title, name: visite.id_etab.toString() + ' - ' + visite.title,
photoCount: objectbox.getVisitPhotoTaken(visite.id_visite), photoCount: objectbox.getVisitPhotoTaken(visite.id_visite),
date: DateFormat('EEEE d MMMM HH:mm', 'fr_FR') date: DateFormat('EEEE d MMMM HH:mm', SharedPrefs().locale)
.format(visite.date_visite), .format(visite.date_visite),
image: visite.url_photo_principale, image: visite.url_photo_principale,
type_visite: visite.type_visite, type_visite: visite.type_visite,
@ -91,7 +91,7 @@ class VisitModel {
id_visite: visite.id_visite, id_visite: visite.id_visite,
name: visite.id_etab.toString() + ' - ' + visite.title, name: visite.id_etab.toString() + ' - ' + visite.title,
photoCount: objectbox.getVisitPhotoTaken(visite.id_visite), photoCount: objectbox.getVisitPhotoTaken(visite.id_visite),
date: DateFormat('EEEE d MMMM HH:mm', 'fr_FR') date: DateFormat('EEEE d MMMM HH:mm', SharedPrefs().locale)
.format(visite.date_visite), .format(visite.date_visite),
image: visite.url_photo_principale, image: visite.url_photo_principale,
type_visite: visite.type_visite, type_visite: visite.type_visite,

View File

@ -1,4 +1,5 @@
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter/material.dart';
class SharedPrefs { class SharedPrefs {
static late SharedPreferences _sharedPrefs; static late SharedPreferences _sharedPrefs;
@ -241,4 +242,24 @@ class SharedPrefs {
set systemVersion(String value) { set systemVersion(String value) {
_sharedPrefs.setString('key_system_version', value); _sharedPrefs.setString('key_system_version', value);
} }
/// get/set language code
String get lCode => _sharedPrefs.getString('lcode') ?? "fr";
set lCode(String value) {
_sharedPrefs.setString('lcode', value);
}
/// get/set language country code
String get cCode => _sharedPrefs.getString('ccode') ?? "FR";
set cCode(String value) {
_sharedPrefs.setString('ccode', value);
}
/// get language object
Locale get language => Locale(lCode, cCode);
/// get locale object
String get locale => lCode + "_" + cCode;
} }

View File

@ -1,3 +1,4 @@
import 'package:mobdr/cubit/language/app_localizations.dart';
import 'package:mobdr/config/constant.dart'; import 'package:mobdr/config/constant.dart';
import 'package:mobdr/config/global_style.dart'; import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/service/shared_prefs.dart'; import 'package:mobdr/service/shared_prefs.dart';
@ -27,7 +28,7 @@ class _AboutPageState extends State<AboutPage> {
), ),
elevation: GlobalStyle.appBarElevation, elevation: GlobalStyle.appBarElevation,
title: Text( title: Text(
'About', AppLocalizations.of(context)!.translate('i18n_menu_about'),
style: GlobalStyle.appBarTitle, style: GlobalStyle.appBarTitle,
), ),
backgroundColor: GlobalStyle.appBarBackgroundColor, backgroundColor: GlobalStyle.appBarBackgroundColor,

View File

@ -6,6 +6,7 @@ import 'package:mobdr/ui/reusable/reusable_widget.dart';
import 'package:mobdr/db/box_log.dart'; import 'package:mobdr/db/box_log.dart';
import 'package:mobdr/service/plausible.dart'; import 'package:mobdr/service/plausible.dart';
import 'package:mobdr/service/logger_util.dart'; import 'package:mobdr/service/logger_util.dart';
import 'package:mobdr/cubit/language/app_localizations.dart';
class LogPage extends StatefulWidget { class LogPage extends StatefulWidget {
@override @override
@ -123,7 +124,7 @@ class _LogPageState extends State<LogPage> {
), ),
elevation: GlobalStyle.appBarElevation, elevation: GlobalStyle.appBarElevation,
title: Text( title: Text(
'Show logs', AppLocalizations.of(context)!.translate('i18n_menu_show_logs'),
style: GlobalStyle.appBarTitle, style: GlobalStyle.appBarTitle,
), ),
backgroundColor: GlobalStyle.appBarBackgroundColor, backgroundColor: GlobalStyle.appBarBackgroundColor,

View File

@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
import 'package:mobdr/config/constant.dart'; import 'package:mobdr/config/constant.dart';
import 'package:mobdr/config/global_style.dart'; import 'package:mobdr/config/global_style.dart';
import 'package:mobdr/main.dart';
import 'package:mobdr/cubit/language/app_localizations.dart';
import 'package:mobdr/ui/reusable/reusable_widget.dart'; import 'package:mobdr/ui/reusable/reusable_widget.dart';
import 'package:mobdr/service/shared_prefs.dart'; import 'package:mobdr/service/shared_prefs.dart';
import 'package:mobdr/service/plausible.dart'; import 'package:mobdr/service/plausible.dart';
import 'package:mobdr/service/logger_util.dart'; import 'package:mobdr/service/logger_util.dart';
import 'package:mobdr/events.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
@override @override
@ -63,7 +66,7 @@ class _SettingsPageState extends State<SettingsPage> {
), ),
elevation: GlobalStyle.appBarElevation, elevation: GlobalStyle.appBarElevation,
title: Text( title: Text(
'Settings', AppLocalizations.of(context)!.translate('i18n_menu_settings'),
style: GlobalStyle.appBarTitle, style: GlobalStyle.appBarTitle,
), ),
backgroundColor: GlobalStyle.appBarBackgroundColor, backgroundColor: GlobalStyle.appBarBackgroundColor,
@ -100,7 +103,9 @@ class _SettingsPageState extends State<SettingsPage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Photo quality', Text(
AppLocalizations.of(context)!
.translate('i18n_label_photo_quality'),
style: TextStyle(fontSize: 15, color: CHARCOAL)), style: TextStyle(fontSize: 15, color: CHARCOAL)),
Row( Row(
children: [ children: [
@ -127,7 +132,8 @@ class _SettingsPageState extends State<SettingsPage> {
child: SwitchListTile( child: SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text( title: Text(
'Photo resizing', AppLocalizations.of(context)!
.translate('i18n_label_photo_resizing'),
style: TextStyle(fontSize: 15, color: CHARCOAL), style: TextStyle(fontSize: 15, color: CHARCOAL),
), ),
value: _photoResizing, value: _photoResizing,
@ -146,7 +152,8 @@ class _SettingsPageState extends State<SettingsPage> {
child: SwitchListTile( child: SwitchListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
title: Text( title: Text(
'Play sound when taking a photo', AppLocalizations.of(context)!
.translate('i18n_label_sound_photo'),
style: TextStyle(fontSize: 15, color: CHARCOAL), style: TextStyle(fontSize: 15, color: CHARCOAL),
), ),
value: _photoSound, value: _photoSound,
@ -183,7 +190,10 @@ class _SettingsPageState extends State<SettingsPage> {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text('Language', style: TextStyle(fontSize: 15, color: CHARCOAL)), Text(
AppLocalizations.of(context)!
.translate('i18n_label_language'),
style: TextStyle(fontSize: 15, color: CHARCOAL)),
Row( Row(
children: [ children: [
Text( Text(
@ -253,7 +263,8 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (String? value) { onChanged: (String? value) {
setState(() { setState(() {
_photoQuality = value!; _photoQuality = value!;
SharedPrefs().photoQuality = value; SharedPrefs().photoQuality =
value; // TODO : Passer par un numéro plutot qu'une chaine qui sera traduite ...
}); });
Navigator.pop(context); Navigator.pop(context);
}, },
@ -309,12 +320,15 @@ class _SettingsPageState extends State<SettingsPage> {
onChanged: (String? value) { onChanged: (String? value) {
setState(() { setState(() {
_language = value!; _language = value!;
switch (value) { switch (value) {
case 'English': case 'English':
SharedPrefs().langage = 'en'; SharedPrefs().langage = 'en';
eventBus.fire(ChangeLocaleEvent('en'));
break; break;
case 'French': case 'French':
SharedPrefs().langage = 'fr'; SharedPrefs().langage = 'fr';
eventBus.fire(ChangeLocaleEvent('fr'));
break; break;
default: default:
_language = 'French'; _language = 'French';

View File

@ -16,6 +16,7 @@ import 'package:flutter/material.dart';
import 'package:mobdr/ui/authentication/signin.dart'; import 'package:mobdr/ui/authentication/signin.dart';
import 'dart:convert'; import 'dart:convert';
import 'package:mobdr/main.dart'; import 'package:mobdr/main.dart';
import 'package:mobdr/cubit/language/app_localizations.dart';
class TabAccountPage extends StatefulWidget { class TabAccountPage extends StatefulWidget {
@override @override
@ -51,7 +52,7 @@ class _TabAccountPageState extends State<TabAccountPage>
), ),
elevation: GlobalStyle.appBarElevation, elevation: GlobalStyle.appBarElevation,
title: Text( title: Text(
'Account', AppLocalizations.of(context)!.translate('i18n_menu_account'),
style: GlobalStyle.appBarTitle, style: GlobalStyle.appBarTitle,
), ),
backgroundColor: GlobalStyle.appBarBackgroundColor, backgroundColor: GlobalStyle.appBarBackgroundColor,
@ -73,13 +74,22 @@ class _TabAccountPageState extends State<TabAccountPage>
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
children: [ children: [
_createAccountInformation(), _createAccountInformation(),
_createListMenu('Settings', SettingsPage()), _createListMenu(
AppLocalizations.of(context)!.translate('i18n_menu_settings'),
SettingsPage()),
_reusableWidget.divider1(), _reusableWidget.divider1(),
_createListMenu('About', AboutPage()), _createListMenu(
AppLocalizations.of(context)!.translate('i18n_menu_about'),
AboutPage()),
_reusableWidget.divider1(), _reusableWidget.divider1(),
_createListMenu('Show logs', LogPage()), _createListMenu(
AppLocalizations.of(context)!.translate('i18n_menu_show_logs'),
LogPage()),
_reusableWidget.divider1(), _reusableWidget.divider1(),
_createListMenu('Check version', UpdateCheckPage()), _createListMenu(
AppLocalizations.of(context)!
.translate('i18n_menu_check_version'),
UpdateCheckPage()),
_reusableWidget.divider1(), _reusableWidget.divider1(),
Container( Container(
margin: EdgeInsets.fromLTRB(0, 18, 0, 0), margin: EdgeInsets.fromLTRB(0, 18, 0, 0),
@ -111,7 +121,9 @@ class _TabAccountPageState extends State<TabAccountPage>
Icon(Icons.power_settings_new, Icon(Icons.power_settings_new,
size: 20, color: ASSENT_COLOR), size: 20, color: ASSENT_COLOR),
SizedBox(width: 8), SizedBox(width: 8),
Text('Sign Out', Text(
AppLocalizations.of(context)!
.translate('i18n_label_sign_out'),
style: TextStyle(fontSize: 15, color: ASSENT_COLOR)), style: TextStyle(fontSize: 15, color: ASSENT_COLOR)),
], ],
), ),

View File

@ -11,6 +11,7 @@ import 'package:mobdr/config/constant.dart';
import 'package:mobdr/events.dart'; import 'package:mobdr/events.dart';
import 'package:mobdr/service/plausible.dart'; import 'package:mobdr/service/plausible.dart';
import 'package:mobdr/service/logger_util.dart'; import 'package:mobdr/service/logger_util.dart';
import 'package:mobdr/cubit/language/app_localizations.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@override @override
@ -144,7 +145,7 @@ class _HomePageState extends State<HomePage>
icon: Icon(Icons.web), icon: Icon(Icons.web),
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
label: 'Account', label: AppLocalizations.of(context)!.translate('i18n_menu_account'),
icon: Icon(Icons.person_outline), icon: Icon(Icons.person_outline),
), ),
], ],

View File

@ -8,7 +8,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:badges/badges.dart' as badges; import 'package:badges/badges.dart' as badges;
import 'package:mobdr/main.dart'; import 'package:mobdr/main.dart';
@ -39,13 +38,12 @@ class _TabHomePageState extends State<TabHomePage>
late StreamSubscription subVisitPhotoCountEvent; late StreamSubscription subVisitPhotoCountEvent;
late StreamSubscription subRefreshCalendarEvent; late StreamSubscription subRefreshCalendarEvent;
late StreamSubscription subChangeLocaleEvent;
// keep the state to do not refresh when switch navbar // keep the state to do not refresh when switch navbar
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
String defaultLang = 'en';
DateTime _todayDate = DateTime.now(); DateTime _todayDate = DateTime.now();
late LanguageCubit _languageCubit; late LanguageCubit _languageCubit;
@ -60,12 +58,6 @@ class _TabHomePageState extends State<TabHomePage>
_languageCubit = BlocProvider.of<LanguageCubit>(context); _languageCubit = BlocProvider.of<LanguageCubit>(context);
_getLocale().then((val) {
setState(() {
defaultLang = val!;
});
});
loadData().then((_) { loadData().then((_) {
setState(() { setState(() {
// Listen refresh photo count event // Listen refresh photo count event
@ -97,6 +89,26 @@ class _TabHomePageState extends State<TabHomePage>
loadData(); loadData();
}); });
}); });
// Listen change locale event
subChangeLocaleEvent = eventBus.on<ChangeLocaleEvent>().listen((e) {
setState(() {
switch (e.language) {
case 'en':
SharedPrefs().lCode = 'en';
SharedPrefs().cCode = 'US';
_languageCubit.changeLanguage(SharedPrefs().language);
break;
case 'fr':
SharedPrefs().lCode = 'fr';
SharedPrefs().cCode = 'FR';
_languageCubit.changeLanguage(SharedPrefs().language);
break;
}
loadData();
});
});
}); });
} }
@ -104,30 +116,17 @@ class _TabHomePageState extends State<TabHomePage>
void dispose() { void dispose() {
subRefreshCalendarEvent.cancel(); subRefreshCalendarEvent.cancel();
subVisitPhotoCountEvent.cancel(); subVisitPhotoCountEvent.cancel();
subChangeLocaleEvent.cancel();
super.dispose(); super.dispose();
} }
Future<String?> _getLocale() async {
final SharedPreferences _pref = await SharedPreferences.getInstance();
String? lCode = _pref.getString('lCode');
return lCode;
}
void changeLocale(Locale locale) async {
final SharedPreferences _pref = await SharedPreferences.getInstance();
await _pref.setString('lCode', locale.languageCode);
await _pref.setString('cCode', locale.countryCode!);
_languageCubit.changeLanguage(locale);
defaultLang = locale.languageCode;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// if we used AutomaticKeepAliveClientMixin, we must call super.build(context); // if we used AutomaticKeepAliveClientMixin, we must call super.build(context);
super.build(context); super.build(context);
final double boxImageSize = (MediaQuery.of(context).size.width / 3); final double boxImageSize = (MediaQuery.of(context).size.width / 3);
final formattedDate = final formattedDate =
DateFormat('EEEE d MMMM y', 'fr_FR').format(_todayDate); DateFormat('EEEE d MMMM y', SharedPrefs().locale).format(_todayDate);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -136,7 +135,7 @@ class _TabHomePageState extends State<TabHomePage>
title: Align( title: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
AppLocalizations.of(context)!.translate('i18n_hello')! + AppLocalizations.of(context)!.translate('i18n_label_hello') +
', ${SharedPrefs().prenom}', ', ${SharedPrefs().prenom}',
style: GlobalStyle.appBarTitle, style: GlobalStyle.appBarTitle,
), ),

View File

@ -206,11 +206,5 @@ flutter:
- assets/lang/fr.json - assets/lang/fr.json
- assets/lang/en.json - assets/lang/en.json
- assets/lang/id.json
- assets/lang/ar.json
- assets/lang/zh.json
- assets/lang/hi.json
- assets/lang/th.json
- assets/lang/tk.json
- assets/sounds/camera-shutter-click.mp3 - assets/sounds/camera-shutter-click.mp3