Skip to content

Commit

Permalink
Merge pull request #6 from Explicit12/Implement-internationalization-…
Browse files Browse the repository at this point in the history
…feature

Implement internationalization feature
  • Loading branch information
Explicit12 authored Apr 27, 2024
2 parents 031a444 + fa2105b commit e59266f
Show file tree
Hide file tree
Showing 15 changed files with 257 additions and 26 deletions.
16 changes: 16 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createLocale, LocaleSymbol } from "./src/composable.locale";

export { default as createVueI18nAdapter } from "./src/adatper.locale/vue-i18n";
export { default as defaultEnLocale } from "./src/adatper.locale/locales/en";

export function createVueless(options = {}) {
const locale = createLocale(options.locale);

const install = (app) => {
app.provide(LocaleSymbol, locale);
};

return {
install,
};
}
21 changes: 21 additions & 0 deletions src/adatper.locale/locales/en.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import selectConfig from "../../ui.form-select/configs/default.config";
import switchConfig from "../../ui.form-switch/configs/default.config";
import inputFileConfig from "../../ui.form-input-file/configs/default.config";
import dropdownListConfig from "../../ui.dropdown-list/configs/default.config";
import modalConfirmConfig from "../../ui.container-modal-confirm/configs/default.config";
import tableConfig from "../../ui.data-table/configs/default.config";
import calendarConfig from "../../ui.form-calendar/configs/default.config";
import datepickerConfig from "../../ui.form-date-picker/configs/default.config";
import datepickerRangeConfig from "../../ui.form-date-picker-range/configs/default.config";

export default {
USelect: selectConfig.i18n,
USwitch: switchConfig.i18n,
UInputFile: inputFileConfig.i18n,
UDropdownList: dropdownListConfig.i18n,
UModalConfirm: modalConfirmConfig.i18n,
UTable: tableConfig.i18n,
UCalendar: calendarConfig.i18n,
UDatePicker: datepickerConfig.i18n,
UDatePickerRange: datepickerRangeConfig.i18n,
};
11 changes: 11 additions & 0 deletions src/adatper.locale/vue-i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function createVueI18nAdapter(i18n) {
return {
name: "vue-i18n",
locale: i18n.global.locale,
fallback: i18n.global.fallbackLocale,
messages: i18n.global.messages,
t: (key, ...params) => i18n.global.t(key, params),
tm: i18n.global.tm,
n: i18n.global.n,
};
}
117 changes: 117 additions & 0 deletions src/adatper.locale/vueless.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { shallowRef, ref } from "vue";

import en from "./locales/en";

const FALLBACK_LOCALE_CODE = "en";

export default function createVuelessAdapter(options) {
const current = shallowRef(options?.locale ?? FALLBACK_LOCALE_CODE);
const fallback = shallowRef(options?.fallback ?? FALLBACK_LOCALE_CODE);

const messages = ref({ en, ...options?.messages });

return {
name: "vueless",
locale: current,
fallback,
messages,
t: createTranslateFunction(current, fallback, messages),
tm: createTranslateMessageFunction(current, fallback, messages),
n: createNumberFunction(current, fallback),
};
}

function createTranslateFunction(current, fallback, messages) {
return (key, ...params) => {
const currentLocale = current.value && messages.value[current.value];
const fallbackLocale = fallback.value && messages.value[fallback.value];

let str = getObjectValueByPath(currentLocale, key, null);

if (!str) {
// eslint-disable-next-line no-console
console.warn(
`Translation key "${key}" not found in "${current.value}", trying fallback locale`,
);
str = getObjectValueByPath(fallbackLocale, key, null);
}

if (!str) {
// eslint-disable-next-line no-console
console.warn(`Translation key "${key}" not found in fallback`);
str = key;
}

if (typeof str !== "string") {
// eslint-disable-next-line no-console
console.warn(`Translation key "${key}" has a non-string value`);
str = key;
}

return replace(str, params);
};
}

function createTranslateMessageFunction(current, fallback, messages) {
return (key) => {
const currentLocale = current.value && messages.value[current.value];
const fallbackLocale = fallback.value && messages.value[fallback.value];

let str = getObjectValueByPath(currentLocale, key, null);

if (str === undefined) {
// eslint-disable-next-line no-console
console.warn(
`Translation key "${key}" not found in "${current.value}", trying fallback locale`,
);
str = getObjectValueByPath(fallbackLocale, key, null);
}

return str;
};
}

const replace = (str, params) => {
return str.replace(/\{(\d+)\}/g, (match, index) => {
return String(params[+index]);
});
};

function createNumberFunction(current, fallback) {
return (value, options) => {
const numberFormat = new Intl.NumberFormat([current.value, fallback.value], options);

return numberFormat.format(value);
};
}

export function getObjectValueByPath(obj, path, fallback) {
if (obj == null || !path || typeof path !== "string") return fallback;
if (obj[path] !== undefined) return obj[path];
path = path.replace(/\[(\w+)\]/g, ".$1"); // convert indexes to properties
path = path.replace(/^\./, ""); // strip a leading dot

return getNestedValue(obj, path.split("."), fallback);
}

export function getNestedValue(obj, path, fallback) {
const last = path.length - 1;

if (last < 0) {
return obj === undefined ? fallback : obj;
}

for (let i = 0; i < last; i++) {
if (obj == null) {
return fallback;
}

obj = obj[path[i]];
}

if (obj == null) {
return fallback;
}

return obj[path[last]] === undefined ? fallback : obj[path[last]];
}
25 changes: 25 additions & 0 deletions src/composable.locale/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { inject } from "vue";
import createVuelessAdapter from "../adatper.locale/vueless";

export const LocaleSymbol = Symbol.for("vueless:locale");

function isLocaleInstance(obj) {
return obj.name !== null;
}

export function createLocale(options) {
const i18n =
options?.adapter && isLocaleInstance(options?.adapter)
? options?.adapter
: createVuelessAdapter(options);

return { ...i18n };
}

export function useLocale() {
const locale = inject(LocaleSymbol);

if (!locale) throw new Error("[vueless] Could not find injected locale instance");

return locale;
}
6 changes: 4 additions & 2 deletions src/ui.container-modal-confirm/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@

<UButton
v-if="cancelButton"
:label="config.i18n.cancel"
:label="props.config?.i18n?.cancel || t('UModalConfirm.cancel')"
variant="thirdary"
filled
:data-cy="`${dataCy}-close`"
Expand All @@ -70,6 +70,7 @@ import UModal from "../ui.container-modal";
import defaultConfig from "./configs/default.config";
import { UModalConfirm } from "./constants/index";
import { useAttrs } from "./composable/attrs.composable";
import { useLocale } from "../composable.locale";
/* Should be a string for correct web-types gen */
defineOptions({ name: "UModalConfirm", inheritAttrs: false });
Expand Down Expand Up @@ -152,8 +153,9 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue", "confirm", "close"]);
const { t } = useLocale();
const {
config,
hasSlotContent,
footerLeftFallbackAttrs,
modalAttrs,
Expand Down
6 changes: 5 additions & 1 deletion src/ui.data-table/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ import TableService from "./services/table.service";
import { HYPHEN_SYMBOL, PX_IN_REM } from "../service.ui";
import { UTable } from "./constants";
import { useAttrs } from "./composables/attrs.composable";
import { useLocale } from "../composable.locale";
/* Should be a string for correct web-types gen */
defineOptions({ name: "UTable", inheritAttrs: false });
Expand Down Expand Up @@ -403,6 +404,7 @@ const emit = defineEmits(["clickRow", "update:rows"]);
defineExpose({ clearSelectedItems });
const slots = useSlots();
const { t } = useLocale();
const selectAll = ref(false);
const canSelectAll = ref(true);
Expand Down Expand Up @@ -517,7 +519,9 @@ const hasContentBeforeFirstRowSlot = computed(() => {
});
const emptyTableMsg = computed(() => {
return props.filters ? config.value.i18n.noResultsForFilters : config.value.i18n.noItems;
return props.filters
? props.config?.i18n?.noResultsForFilters || t("UTable.noResultsForFilters")
: props.config?.i18n?.noItems || t("UTable.noItems");
});
watch(selectAll, onChangeSelectAll, { deep: true });
Expand Down
8 changes: 6 additions & 2 deletions src/ui.dropdown-list/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@
:empty-styles="optionClasses"
>
<span v-bind="optionAttrs()">
<span v-text="config.i18n.noDataToShow" />
<span v-text="props.config?.i18n?.noDataToShow || t('UDropdownList.noDataToShow')" />
</span>
</slot>

<!-- Add button -->
<template v-if="addOption">
<div v-bind="addTitleWrapperAttrs" @click="onClickAddOption">
<div v-bind="addTitleAttrs">
{{ config.i18n.add }}
{{ props.config?.i18n?.add || t("UDropdownList.add") }}
<span v-bind="addTitleHotkeyAttrs" v-text="addOptionKeyCombination" />
</div>
</div>
Expand All @@ -95,6 +95,8 @@ import UIService, { getRandomId, isMac } from "../service.ui";
import usePointer from "./composables/usePointer";
import useAttrs from "./composables/attrs.composable";
import { useLocale } from "../composable.locale";
import defaultConfig from "./configs/default.config.js";
import { UDropdownList } from "./constants";
Expand Down Expand Up @@ -229,6 +231,8 @@ const {
optionContentAttrs,
} = useAttrs(props);
const { t } = useLocale();
defineExpose({ pointerSet, pointerBackward, pointerForward, pointerReset, addPointerElement });
const addOptionKeyCombination = computed(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/ui.form-calendar/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ import {
} from "./services/date.service";
import useAttrs from "./composables/attrs.composable";
import { useLocale } from "../composable.locale";
import {
UCalendar,
Expand Down Expand Up @@ -288,6 +289,8 @@ const emit = defineEmits([
"formattedDateChange",
]);
const { tm } = useLocale();
const {
config,
wrapperAttrs,
Expand Down Expand Up @@ -342,7 +345,8 @@ const isCurrentView = computed(() => ({
}));
const locale = computed(() => {
const currentLocale = props.config.i18n || config.value.i18n;
const currentLocale = props.config.i18n || tm("UCalendar");
const formattedLocale = {
...currentLocale,
months: {
Expand All @@ -359,7 +363,7 @@ const locale = computed(() => {
});
const userFormatLocale = computed(() => {
const currentLocale = props.config.i18n || config.value.i18n;
const currentLocale = props.config.i18n || tm("UCalendar");
const formattedLocale = {
...currentLocale,
Expand Down
7 changes: 5 additions & 2 deletions src/ui.form-date-picker-range/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ import {
import { wrongDateFormat, wrongMonthNumber, wrongDayNumber } from "./services/validation.service";
import useAttrs from "./composables/attrs.composable";
import { useLocale } from "../composable.locale";
import defaultConfig from "./configs/default.config";
import {
UDatePickerRange,
Expand Down Expand Up @@ -383,6 +385,7 @@ const {
inputRangeErrorAttrs,
} = useAttrs(props, { isShownMenu });
const store = useStore();
const { tm } = useLocale();
const calendarValue = ref(props.modelValue);
const activeDate = ref(
Expand Down Expand Up @@ -414,7 +417,7 @@ const isMobileDevice = computed(() => store.getters["breakpoint/isMobileDevice"]
const rangeInputName = computed(() => `rangeInput-${props.id}`);
const locale = computed(() => {
const currentLocale = props.config.i18n || config.value.i18n;
const currentLocale = props.config.i18n || tm("UDatePickerRange");
const formattedLocale = {
...currentLocale,
months: {
Expand All @@ -431,7 +434,7 @@ const locale = computed(() => {
});
const userFormatLocale = computed(() => {
const currentLocale = props.config.i18n || config.value.i18n;
const currentLocale = props.config.i18n || tm("UDatePickerRange");
const formattedLocale = {
...currentLocale,
Expand Down
6 changes: 5 additions & 1 deletion src/ui.form-date-picker/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import {
} from "../ui.form-calendar/services/date.service";
import useAttrs from "./composables/attrs.composable";
import { useLocale } from "../composable.locale";
import defaultConfig from "./configs/default.config";
import { UDatePicker } from "./constants";
Expand Down Expand Up @@ -201,6 +203,8 @@ const emit = defineEmits(["update:modelValue", "input"]);
const STANDARD_USER_FORMAT = "l, j F, Y";
const { tm } = useLocale();
const isShownCalendar = ref(false);
const userFormatDate = ref("");
const formattedDate = ref("");
Expand Down Expand Up @@ -254,7 +258,7 @@ function onBlur(event) {
function formatUserDate(data) {
if (props.dateFormat !== STANDARD_USER_FORMAT) return data;
const currentLocale = props.config.i18n || config.value.i18n;
const currentLocale = props.config.i18n || tm("UDatePicker");
let prefix = "";
const formattedDate = data.charAt(0).toUpperCase() + data.toLowerCase().slice(1);
Expand Down
Loading

0 comments on commit e59266f

Please sign in to comment.