Skip to content

Commit 948b3dd

Browse files
authored
[6.x] Ability to select the date formatting locale (#14372)
1 parent d6a494d commit 948b3dd

9 files changed

Lines changed: 169 additions & 1 deletion

File tree

lang/en/messages.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@
198198
'phpinfo_utility_description' => 'Check PHP configuration settings and installed modules.',
199199
'plus_count_more' => '+ :count more',
200200
'preference_confirm_dirty_navigation_instructions' => 'Whether you should get a warning when trying to leave the page when there are unsaved changes.',
201+
'preference_formatting_locale_invalid' => 'Please enter a valid locale code.',
202+
'preference_formatting_locale_instructions' => 'The locale used for formatting values in the control panel, such as dates and numbers.',
201203
'preference_locale_instructions' => 'The preferred language for the control panel.',
202204
'preference_start_page_instructions' => 'The page to be shown when logging into the control panel.',
203205
'preference_strict_accessibility_instructions' => 'We\'ve designed the control panel with accessibility in mind, aiming to meet WCAG 2.2 guidelines where possible. Enable this option to apply stricter accessibility rules by increasing form border contrast.',

resources/js/bootstrap/fieldtypes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Routes from '../components/collections/Routes.vue';
1717
import TitleFormats from '../components/collections/TitleFormats.vue';
1818
import ColorFieldtype from '../components/fieldtypes/ColorFieldtype.vue';
1919
import DateFieldtype from '../components/fieldtypes/DateFieldtype.vue';
20+
import FormattingLocalesFieldtype from '../components/fieldtypes/FormattingLocalesFieldtype.vue';
2021
import DateIndexFieldtype from '../components/fieldtypes/DateIndexFieldtype.vue';
2122
import DictionaryFieldtype from '../components/fieldtypes/DictionaryFieldtype.vue';
2223
import DictionaryIndexFieldtype from '../components/fieldtypes/DictionaryIndexFieldtype.vue';
@@ -88,6 +89,7 @@ export default function registerFieldtypes(app) {
8889
app.component('collection_title_formats-fieldtype', TitleFormats);
8990
app.component('color-fieldtype', ColorFieldtype);
9091
app.component('date-fieldtype', DateFieldtype);
92+
app.component('formatting_locales-fieldtype', FormattingLocalesFieldtype);
9193
app.component('date-fieldtype-index', DateIndexFieldtype);
9294
app.component('dictionary-fieldtype', DictionaryFieldtype);
9395
app.component('dictionary-fieldtype-index', DictionaryIndexFieldtype);

resources/js/bootstrap/statamic.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,13 @@ export default {
176176
contrast.initialize(this.initialConfig.user?.preferences?.strict_accessibility);
177177
preferences.initialize(this.initialConfig.user?.preferences, this.initialConfig.defaultPreferences);
178178

179+
const formattingLocale = this.initialConfig.user?.preferences?.formatting_locale;
180+
if (formattingLocale === 'language') {
181+
dateFormatter.setDefaultLocale(this.initialConfig.translationLocale);
182+
} else if (formattingLocale) {
183+
dateFormatter.setDefaultLocale(formattingLocale);
184+
}
185+
179186
bootingCallbacks.forEach((callback) => callback(this));
180187
bootingCallbacks = [];
181188

resources/js/components/DateFormatter.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,21 @@ export default class DateFormatter {
102102
return this.date(date).options(options).toString();
103103
}
104104

105+
withLocale(locale, callback) {
106+
const previousLocale = DateFormatter.defaultLocale;
107+
this.setDefaultLocale(locale);
108+
109+
try {
110+
return callback(this);
111+
} finally {
112+
this.setDefaultLocale(previousLocale);
113+
}
114+
}
115+
116+
setDefaultLocale(locale) {
117+
DateFormatter.defaultLocale = locale;
118+
}
119+
105120
get locale() {
106121
return DateFormatter.defaultLocale ?? Intl.DateTimeFormat().resolvedOptions().locale;
107122
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<template>
2+
<Combobox
3+
clearable
4+
taggable
5+
:options="options"
6+
:read-only="isReadOnly"
7+
:placeholder="null"
8+
:model-value="value"
9+
@update:modelValue="comboboxUpdated"
10+
>
11+
<template #selected-option="{ option: { value, label, sample } }">
12+
<template v-if="value === 'language'">{{ label }}</template>
13+
<div v-else class="w-full flex justify-between">
14+
<div class="text-start flex-1">
15+
{{ getLabel(value) }}
16+
<span class="ms-4 text-gray-500 dark:text-gray-400" v-text="value" />
17+
</div>
18+
<span class="text-gray-500 dark:text-gray-400" v-text="getSample(value)" />
19+
</div>
20+
</template>
21+
<template #option="{ value, label, sample }">
22+
<template v-if="value === 'language'">{{ label }}</template>
23+
<div v-else class="w-full flex justify-between">
24+
<div class="text-start flex-1">
25+
{{ label }}
26+
<span class="ms-4 text-gray-500 dark:text-gray-400" v-text="value" />
27+
</div>
28+
<span class="text-gray-500 dark:text-gray-400" v-text="sample" />
29+
</div>
30+
</template>
31+
</Combobox>
32+
</template>
33+
34+
<script setup>
35+
import Fieldtype from '@/components/fieldtypes/fieldtype.js';
36+
import { dateFormatter, toast } from '@api';
37+
import { Combobox } from '@/components/ui';
38+
import { computed } from 'vue';
39+
40+
const emit = defineEmits(Fieldtype.emits);
41+
const props = defineProps(Fieldtype.props);
42+
const { isReadOnly, update } = Fieldtype.use(emit, props);
43+
44+
const candidateLocales = [
45+
'ar', 'az', 'cs', 'da', 'de', 'de-CH', 'en', 'es', 'et', 'fa', 'fr',
46+
'hu', 'id', 'it', 'ja', 'ms', 'nb', 'nl', 'pl', 'pt', 'pt-BR', 'ru',
47+
'sl', 'sv', 'tr', 'uk', 'vi', 'zh-CN', 'zh-TW',
48+
];
49+
50+
const displayNames = typeof Intl.DisplayNames !== 'undefined'
51+
? new Intl.DisplayNames([document.documentElement.lang || 'en'], { type: 'language' })
52+
: null;
53+
54+
const options = computed(() => {
55+
const locales = Intl.DateTimeFormat.supportedLocalesOf(candidateLocales);
56+
57+
const formatted = locales.map((locale) => ({
58+
value: locale,
59+
label: getLabel(locale),
60+
sample: getSample(locale),
61+
}));
62+
63+
return [
64+
{ value: 'language', label: __('Same as language') },
65+
...formatted,
66+
];
67+
});
68+
69+
function getLabel(locale) {
70+
return displayNames?.of(locale.split('-')[0]);
71+
}
72+
73+
function getSample(locale) {
74+
return dateFormatter.withLocale(locale, (formatter) => formatter.format(new Date, 'datetime'));
75+
}
76+
77+
function comboboxUpdated(value) {
78+
if (value && !isValidLocale(value)) {
79+
update(null);
80+
toast.error(__('statamic::messages.preference_formatting_locale_invalid'));
81+
return;
82+
}
83+
84+
update(value || null);
85+
}
86+
87+
function isValidLocale(value) {
88+
if (value === 'language') {
89+
return true;
90+
}
91+
92+
try {
93+
return Intl.DateTimeFormat.supportedLocalesOf([value]).length > 0;
94+
} catch {
95+
return false;
96+
}
97+
}
98+
</script>

resources/js/tests/components/DateFormatter.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,27 @@ test('it can statically format', () => {
4747
expect(DateFormatter.format('1995-03-13T22:45:19Z', { year: 'numeric' })).toBe('1995');
4848
});
4949

50+
test('it can temporarily format with locale using callback', () => {
51+
const formatter = new DateFormatter();
52+
DateFormatter.defaultLocale = 'en-us';
53+
54+
const result = formatter.withLocale('de', (instance) => instance.format('2021-12-25T12:13:14Z', 'datetime'));
55+
56+
expect(result).toBe('25.12.2021, 12:13');
57+
expect(DateFormatter.defaultLocale).toBe('en-us');
58+
});
59+
60+
test('it resets locale after withLocale callback throws', () => {
61+
const formatter = new DateFormatter();
62+
DateFormatter.defaultLocale = 'en-us';
63+
64+
expect(() => formatter.withLocale('de', () => {
65+
throw new Error('boom');
66+
})).toThrow('boom');
67+
68+
expect(DateFormatter.defaultLocale).toBe('en-us');
69+
});
70+
5071
test('it can format on the instance', () => {
5172
expect(new DateFormatter().format('1995-03-13T22:45:19Z')).toBe('3/13/1995, 10:45 PM');
5273
expect(new DateFormatter().format('1995-03-13T22:45:19Z', { year: 'numeric' })).toBe('1995');
@@ -186,6 +207,12 @@ test('it can get the locale', () => {
186207
expect(new DateFormatter().locale).toBe('fr');
187208
});
188209

210+
test('it can set the default locale via setDefaultLocale', () => {
211+
new DateFormatter().setDefaultLocale('de');
212+
expect(DateFormatter.defaultLocale).toBe('de');
213+
expect(new DateFormatter().locale).toBe('de');
214+
});
215+
189216
test.each([
190217
['en', 'date', '12/25/2021'],
191218
['en', 'time', '12:13 PM'],
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Statamic\Fieldtypes;
4+
5+
use Statamic\Fields\Fieldtype;
6+
7+
class FormattingLocales extends Fieldtype
8+
{
9+
protected $selectable = false;
10+
}

src/Preferences/CorePreferences.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ public function boot()
1414
{
1515
Preference::register('locale', [
1616
'type' => 'select',
17-
'display' => __('Locale'),
17+
'display' => __('Language'),
1818
'instructions' => __('statamic::messages.preference_locale_instructions'),
1919
'clearable' => true,
2020
'label_html' => true,
2121
'options' => $this->localeOptions(),
2222
]);
2323

24+
Preference::register('formatting_locale', [
25+
'type' => 'formatting_locales',
26+
'display' => __('Formatting Locale'),
27+
'instructions' => __('statamic::messages.preference_formatting_locale_instructions'),
28+
]);
29+
2430
Preference::register('start_page', [
2531
'type' => 'text',
2632
'display' => __('Start Page'),

src/Providers/ExtensionServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class ExtensionServiceProvider extends ServiceProvider
8383
Fieldtypes\FieldDisplay::class,
8484
Fieldtypes\Files::class,
8585
Fieldtypes\Floatval::class,
86+
Fieldtypes\FormattingLocales::class,
8687
Fieldtypes\GlobalSetSites::class,
8788
Fieldtypes\Grid::class,
8889
Fieldtypes\Group::class,

0 commit comments

Comments
 (0)