Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin UI: Use l10n translations everywhere #692

Merged
merged 55 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a990a3c
feat: move all texts to l10n file
stamenione Jun 11, 2024
76fa310
fix: add trailing comma
stamenione Jun 11, 2024
a9a7c36
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
stamenione Jun 11, 2024
c1dc59e
fix: remove fixed names
stamenione Jun 11, 2024
4f3bddc
fix: localization structure and add context translation keys
stamenione Jun 11, 2024
45f0f4b
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 11, 2024
9b3ae28
chore: extract more context text
stamenione Jun 11, 2024
7545b72
Merge branch 'NMSHDB-143-Admin-UI-Preparation-for-translation' of htt…
stamenione Jun 11, 2024
45c083a
chore: add space
stamenione Jun 11, 2024
9ac6256
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 11, 2024
d37bfdf
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 12, 2024
e4ed469
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 13, 2024
aa3f1f6
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 17, 2024
f0a2da4
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 17, 2024
1cf85ee
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 17, 2024
24198fc
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 17, 2024
a3a648f
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 18, 2024
011a844
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 18, 2024
a6f2562
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 19, 2024
172cbe5
fix: change from camel case to underscore notation
stamenione Jun 20, 2024
cee6d77
chore: addition to camel case to underscore fix
stamenione Jun 20, 2024
5bf5497
fix: remove localization from assert
stamenione Jun 20, 2024
e3b3a6a
fix: remove invalid state
stamenione Jun 21, 2024
76495f0
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 24, 2024
ef87446
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 24, 2024
f1f1665
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 24, 2024
033fd5f
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 24, 2024
b60b489
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 24, 2024
5164ca1
fix: remove signs from translation
stamenione Jun 25, 2024
3c2ffaa
fix: add sign to translated text
stamenione Jun 25, 2024
34a9e28
revert: undo the space for the app title
stamenione Jun 25, 2024
2732e04
fix: add trailing comma
stamenione Jun 25, 2024
af67980
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 26, 2024
42e12d2
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 26, 2024
005acf0
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 27, 2024
015a801
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jun 27, 2024
90d629b
fix: make text translatable
stamenione Jun 27, 2024
2c94bda
Merge branch 'NMSHDB-143-Admin-UI-Preparation-for-translation' of htt…
stamenione Jun 27, 2024
1f62482
fix: remove translation within translation
stamenione Jun 27, 2024
5b2c887
fix: remove the rest of translation within translation
stamenione Jun 27, 2024
d3aec19
refactor: translation names
stamenione Jul 1, 2024
6f224e6
fix: add trailing comma
stamenione Jul 1, 2024
fbec8bc
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 1, 2024
437e8e4
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 1, 2024
6db9e77
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 1, 2024
50a6e47
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 1, 2024
11cc7dc
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 2, 2024
8e97b19
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 2, 2024
c379a63
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 2, 2024
ac8a26c
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
stamenione Jul 3, 2024
234bae8
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 3, 2024
b2afd25
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 5, 2024
b604f84
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 5, 2024
4f53715
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 5, 2024
1958f54
Merge branch 'main' into NMSHDB-143-Admin-UI-Preparation-for-translation
mergify[bot] Jul 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions AdminUi/apps/admin_ui/lib/core/modals/add_quota_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
return PopScope(
canPop: !_saving,
child: AlertDialog(
title: const Text('Add Quota'),
title: Text(context.l10n.addQuota),
content: SizedBox(
width: 500,
child: Column(
Expand All @@ -92,37 +92,37 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
value: _selectedMetric,
items: widget.availableMetrics.map((metric) => DropdownMenuItem(value: metric.key, child: Text(metric.displayName))).toList(),
onChanged: _saving ? null : (String? selected) => setState(() => _selectedMetric = selected),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Metric*',
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: '${context.l10n.metric}*',
),
),
Gaps.h24,
TextField(
controller: _maxAmountController,
enabled: !_saving,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Max Amount*',
helperText: 'Only numbers greater or equal to 0 are valid.',
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: '${context.l10n.maxEntities('Amount')}*',
stamenione marked this conversation as resolved.
Show resolved Hide resolved
helperText: context.l10n.max_amount_message,
),
inputFormatters: <TextInputFormatter>[FilteringTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number,
),
Gaps.h24,
DropdownButtonFormField(
value: _selectedPeriod,
items: const [
DropdownMenuItem(value: 'Hour', child: Text('Hour')),
DropdownMenuItem(value: 'Day', child: Text('Day')),
DropdownMenuItem(value: 'Week', child: Text('Week')),
DropdownMenuItem(value: 'Month', child: Text('Month')),
DropdownMenuItem(value: 'Year', child: Text('Year')),
items: [
DropdownMenuItem(value: 'Hour', child: Text(context.l10n.hour)),
DropdownMenuItem(value: 'Day', child: Text(context.l10n.day)),
DropdownMenuItem(value: 'Week', child: Text(context.l10n.week)),
DropdownMenuItem(value: 'Month', child: Text(context.l10n.month)),
DropdownMenuItem(value: 'Year', child: Text(context.l10n.year)),
],
onChanged: _saving ? null : (String? selected) => setState(() => _selectedPeriod = selected),
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Period*',
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: '${context.l10n.period}*',
),
),
if (_errorMessage != null)
Expand All @@ -143,7 +143,7 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
),
FilledButton(
onPressed: _isValid && !_saving ? _addQuota : null,
child: const Text('Add'),
child: Text(context.l10n.add),
),
],
),
Expand All @@ -153,7 +153,7 @@ class _AddQuotaDialogState extends State<_AddQuotaDialog> {
Future<void> _addQuota() async {
setState(() => _saving = true);

assert(_selectedMetric != null && _maxAmount != null && _selectedPeriod != null, 'Invalid state');
assert(_selectedMetric != null && _maxAmount != null && _selectedPeriod != null, 'Invalid State');
stamenione marked this conversation as resolved.
Show resolved Hide resolved

final response = await widget.addQuota(
metricKey: _selectedMetric!,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';

import '../extensions.dart';

Future<bool> showConfirmationDialog({
required BuildContext context,
required String title,
Expand Down Expand Up @@ -27,11 +29,11 @@ class _ConfirmationDialog extends StatelessWidget {
actions: [
OutlinedButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
child: Text(context.l10n.cancel),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
child: Text(context.l10n.confirm),
),
],
);
Expand Down
15 changes: 8 additions & 7 deletions AdminUi/apps/admin_ui/lib/core/modals/settings_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:watch_it/watch_it.dart';

import '../constants.dart';
import '../extensions.dart';
import '../models/models.dart';

Future<void> openSettingsDialog(BuildContext context) async {
Expand All @@ -17,26 +18,26 @@ class _SettingsDialog extends StatelessWidget with WatchItMixin {
final themeMode = watchValue((ThemeModeModel x) => x.themeMode);

return AlertDialog(
title: const Text('Settings'),
title: Text(context.l10n.settings),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Theme', style: Theme.of(context).textTheme.bodyLarge),
Text(context.l10n.theme, style: Theme.of(context).textTheme.bodyLarge),
Gaps.h4,
SegmentedButton(
showSelectedIcon: false,
segments: const [
ButtonSegment(value: ThemeMode.light, icon: Icon(Icons.light_mode), label: Text('Light')),
ButtonSegment(value: ThemeMode.system, icon: Icon(Icons.settings), label: Text('System')),
ButtonSegment(value: ThemeMode.dark, icon: Icon(Icons.dark_mode), label: Text('Dark')),
segments: [
ButtonSegment(value: ThemeMode.light, icon: const Icon(Icons.light_mode), label: Text(context.l10n.light)),
ButtonSegment(value: ThemeMode.system, icon: const Icon(Icons.settings), label: Text(context.l10n.system)),
ButtonSegment(value: ThemeMode.dark, icon: const Icon(Icons.dark_mode), label: Text(context.l10n.dark)),
],
selected: {themeMode},
onSelectionChanged: (selected) => GetIt.I<ThemeModeModel>().setThemeMode(selected.first),
),
],
),
actions: [FilledButton(onPressed: () => context.pop(), child: const Text('Close'))],
actions: [FilledButton(onPressed: () => context.pop(), child: Text(context.l10n.close))],
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:data_table_2/data_table_2.dart';
import 'package:flutter/material.dart';

import '../../constants.dart';
import '../../extensions.dart';
import 'identities_data_table_source.dart';

export 'identities_data_table_source.dart';
Expand Down Expand Up @@ -36,26 +37,26 @@ class _IdentitiesDataTableState extends State<IdentitiesDataTable> {
isVerticalScrollBarVisible: true,
renderEmptyRowsInTheEnd: false,
availableRowsPerPage: const [5, 10, 25, 50, 100],
empty: const Text('No identities found.'),
empty: Text(context.l10n.noEntitiesFound('identities')),
errorBuilder: (error) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('An error occurred loading the data.'),
Text(context.l10n.error_data_load_failed),
Gaps.h16,
FilledButton(onPressed: widget.dataSource.refreshDatasource, child: const Text('Retry')),
FilledButton(onPressed: widget.dataSource.refreshDatasource, child: Text(context.l10n.retry)),
],
),
),
columns: <DataColumn2>[
DataColumn2(label: const Text('Address'), size: ColumnSize.L, onSort: _sort),
if (!widget.hideTierColumn) const DataColumn2(label: Text('Tier'), size: ColumnSize.S),
DataColumn2(label: const Text('Created with Client'), onSort: _sort),
DataColumn2(label: const Text('Number of Devices'), onSort: _sort),
DataColumn2(label: const Text('Created at'), size: ColumnSize.S, onSort: _sort),
DataColumn2(label: const Text('Last Login at'), size: ColumnSize.S, onSort: _sort),
DataColumn2(label: const Text('Datawallet version'), onSort: _sort),
DataColumn2(label: const Text('Identity Version'), onSort: _sort),
DataColumn2(label: Text(context.l10n.address), size: ColumnSize.L, onSort: _sort),
if (!widget.hideTierColumn) DataColumn2(label: Text(context.l10n.tier), size: ColumnSize.S),
DataColumn2(label: Text(context.l10n.created_with_client), onSort: _sort),
DataColumn2(label: Text(context.l10n.numberOfEntities('Devices')), onSort: _sort),
DataColumn2(label: Text(context.l10n.createdAt), size: ColumnSize.S, onSort: _sort),
DataColumn2(label: Text(context.l10n.last_login_at), size: ColumnSize.S, onSort: _sort),
DataColumn2(label: Text(context.l10n.entityVersion('Datawallet')), onSort: _sort),
DataColumn2(label: Text(context.l10n.entityVersion('Identity')), onSort: _sort),
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:multi_dropdown/multiselect_dropdown.dart';

import '../../constants.dart';
import '../../extensions.dart';
import '../filters/filters.dart';

class IdentitiesFilter extends StatefulWidget {
Expand Down Expand Up @@ -60,7 +61,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
mainAxisSize: MainAxisSize.min,
children: [
InputField(
label: 'Address',
label: context.l10n.address,
onEnteredText: (String enteredText) {
_filter = _filter.copyWith(address: enteredText.isEmpty ? const Optional.absent() : Optional(enteredText));
widget.onFilterChanged(filter: _filter);
Expand All @@ -69,8 +70,8 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
if (widget.fixedTierId == null) ...[
Gaps.w16,
MultiSelectFilter(
label: 'Tiers',
searchLabel: 'Search Tiers',
label: context.l10n.tiers,
searchLabel: context.l10n.searchEntities('Tiers'),
controller: _tierController,
onOptionSelected: (List<ValueItem<String>> selectedOptions) {
final selectedTiers = selectedOptions.map((item) => item.value!).toList();
Expand All @@ -81,8 +82,8 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
],
Gaps.w16,
MultiSelectFilter(
label: 'Clients',
searchLabel: 'Search Clients',
label: context.l10n.clients,
searchLabel: context.l10n.searchEntities('Clients'),
controller: _clientController,
onOptionSelected: (List<ValueItem<String>> selectedOptions) {
final selectedClients = selectedOptions.map((item) => item.value!).toList();
Expand All @@ -92,7 +93,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
),
Gaps.w16,
NumberFilter(
label: 'Number of Devices',
label: context.l10n.numberOfEntities('Devices'),
onNumberSelected: (FilterOperator operator, String enteredValue) {
final numberOfDevices = FilterOperatorValue(operator, enteredValue);
_filter = _filter.copyWith(numberOfDevices: numberOfDevices.value.isEmpty ? const Optional.absent() : Optional(numberOfDevices));
Expand All @@ -101,7 +102,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
),
Gaps.w16,
DateFilter(
label: 'Created At',
label: context.l10n.createdAt,
onFilterSelected: (FilterOperator operator, DateTime? selectedDate) {
final createdAt = FilterOperatorValue(operator, selectedDate != null ? DateFormat('yyyy-MM-dd').format(selectedDate) : '');
_filter = _filter.copyWith(createdAt: createdAt.value.isEmpty ? const Optional.absent() : Optional(createdAt));
Expand All @@ -110,7 +111,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
),
Gaps.w16,
DateFilter(
label: 'Last Login At',
label: context.l10n.last_login_at,
onFilterSelected: (FilterOperator operator, DateTime? selectedDate) {
final lastLoginAt = FilterOperatorValue(operator, selectedDate != null ? DateFormat('yyyy-MM-dd').format(selectedDate) : '');
_filter = _filter.copyWith(lastLoginAt: lastLoginAt.value.isEmpty ? const Optional.absent() : Optional(lastLoginAt));
Expand All @@ -119,7 +120,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
),
Gaps.w16,
NumberFilter(
label: 'Datawallet Version',
label: context.l10n.entityVersion('Datawallet'),
onNumberSelected: (FilterOperator operator, String enteredValue) {
final datawalletVersion = FilterOperatorValue(operator, enteredValue);
_filter =
Expand All @@ -129,7 +130,7 @@ class _IdentitiesFilterState extends State<IdentitiesFilter> {
),
Gaps.w16,
NumberFilter(
label: 'Identity Version',
label: context.l10n.entityVersion('Identity'),
onNumberSelected: (FilterOperator operator, String enteredValue) {
final identityVersion = FilterOperatorValue(operator, enteredValue);
_filter = _filter.copyWith(identityVersion: identityVersion.value.isEmpty ? const Optional.absent() : Optional(identityVersion));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';

import '../constants.dart';
import '../extensions.dart';
import '../modals/modals.dart';

class QuotasButtonGroup extends StatefulWidget {
Expand Down Expand Up @@ -62,9 +63,9 @@ class _QuotasButtonGroupState extends State<QuotasButtonGroup> {
Future<void> _removeSelectedQuotas() async {
final confirmed = await showConfirmationDialog(
context: context,
title: 'Remove Quotas',
title: context.l10n.removeEntities('Quotas'),
message:
'Are you sure you want to remove the selected quotas from ${widget.identityAddress != null ? 'the identity "${widget.identityAddress}"' : 'the tier "${widget.tierId}"'}?',
'${context.l10n.deletion_of_quota_message} ${widget.identityAddress != null ? '${context.l10n.theIdentity} "${widget.identityAddress}"' : '${context.l10n.the_tier} "${widget.tierId}"'}?',
);

if (!confirmed) return;
Expand All @@ -73,8 +74,8 @@ class _QuotasButtonGroupState extends State<QuotasButtonGroup> {
final result = await _deleteQuota(quota);
if (result.hasError && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('An error occurred while deleting the quota(s). Please try again.'),
SnackBar(
content: Text(context.l10n.errorOccurredWhileDeletingEntities('quota(s)')),
showCloseIcon: true,
),
);
Expand All @@ -87,8 +88,8 @@ class _QuotasButtonGroupState extends State<QuotasButtonGroup> {
widget.selectedQuotas.clear();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Selected quota(s) have been removed.'),
SnackBar(
content: Text(context.l10n.selectedEntityWasRemoved('quota(s)')),
showCloseIcon: true,
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class _ClientsFilterRowState extends State<ClientsFilterRow> {
mainAxisSize: MainAxisSize.min,
children: [
InputField(
label: 'Client ID',
label: context.l10n.client_ID,
onEnteredText: (String enteredText) {
filter = filter.copyWith(clientId: enteredText.isEmpty ? const Optional.absent() : Optional(enteredText));

Expand All @@ -132,7 +132,7 @@ class _ClientsFilterRowState extends State<ClientsFilterRow> {
),
Gaps.w16,
InputField(
label: 'Display Name',
label: context.l10n.display_name,
onEnteredText: (String enteredText) {
filter = filter.copyWith(displayName: enteredText.isEmpty ? const Optional.absent() : Optional(enteredText));

Expand All @@ -141,8 +141,8 @@ class _ClientsFilterRowState extends State<ClientsFilterRow> {
),
Gaps.w16,
MultiSelectFilter(
label: 'Default Tier',
searchLabel: 'Search Tiers',
label: context.l10n.default_tier,
searchLabel: context.l10n.searchEntities('Tiers'),
controller: _tierController,
onOptionSelected: (List<ValueItem<dynamic>> selectedOptions) {
filter = filter.copyWith(
Expand All @@ -154,7 +154,7 @@ class _ClientsFilterRowState extends State<ClientsFilterRow> {
),
Gaps.w16,
NumberFilter(
label: 'Number of Identitites',
label: context.l10n.numberOfEntities('Identities'),
onNumberSelected: (FilterOperator operator, String enteredValue) {
filter = filter.copyWith(
numberOfIdentities: enteredValue.isEmpty ? const Optional.absent() : Optional((operator, int.parse(enteredValue))),
Expand All @@ -165,7 +165,7 @@ class _ClientsFilterRowState extends State<ClientsFilterRow> {
),
Gaps.w16,
DateFilter(
label: 'Created At',
label: context.l10n.createdAt,
onFilterSelected: (FilterOperator operator, DateTime? selectedDate) {
filter = filter.copyWith(
createdAt: selectedDate == null ? const Optional.absent() : Optional((operator, selectedDate)),
Expand Down
Loading
Loading