feat(ui): allow namespace=false to skip i18n translation across components

This commit is contained in:
melvinchia3636
2026-06-24 01:20:31 +08:00
parent 451abae5b3
commit bd1389254c
29 changed files with 174 additions and 171 deletions

View File

@@ -100,36 +100,6 @@ export const contract = {
"NOT_FOUND": true
}
},
"notifyMissing": {
"method": "post",
"description": "Report missing localization keys",
"noAuth": false,
"encrypted": true,
"isDownloadable": false,
"media": null,
"input": {
"body": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"namespace": {
"type": "string"
},
"missingKey": {
"type": "string"
}
},
"required": [
"namespace",
"missingKey"
],
"additionalProperties": false
}
},
"output": {
"NO_CONTENT": true
}
},
"listUnsupportedModules": {
"method": "get",
"description": "List modules that do not support the user's currently selected language",
@@ -299,9 +269,6 @@ export const contract = {
"fontFamily": {
"type": "string"
},
"language": {
"type": "string"
},
"dashboardLayout": {},
"fontScale": {
"type": "number"
@@ -313,6 +280,9 @@ export const contract = {
"bordered": {
"type": "boolean"
},
"language": {
"type": "string"
},
"created": {
"type": "string"
},
@@ -328,9 +298,6 @@ export const contract = {
"collectionName": {
"type": "string"
},
"hasMasterPassword": {
"type": "boolean"
},
"hasJournalMasterPassword": {
"type": "boolean"
},
@@ -355,18 +322,17 @@ export const contract = {
"bgImage",
"backdropFilters",
"fontFamily",
"language",
"dashboardLayout",
"fontScale",
"pinnedFontFamilies",
"borderRadiusMultiplier",
"bordered",
"language",
"created",
"updated",
"id",
"collectionId",
"collectionName",
"hasMasterPassword",
"hasJournalMasterPassword",
"hasAPIKeysMasterPassword",
"twoFAEnabled"

View File

@@ -17,7 +17,7 @@ interface EmptyStateScreenProps {
message:
| {
id: string
namespace?: string
namespace?: string | false
tKey?: string
}
| {
@@ -77,16 +77,18 @@ export function EmptyStateScreen({
weight="semibold"
>
{'id' in message
? t(
`${message.namespace ? `${message.namespace}:` : ''}${[
message.tKey,
'empty',
message.id,
'title'
]
.filter(e => e)
.join('.')}`
)
? message.namespace === false
? message.id
: t(
`${message.namespace ? `${message.namespace}:` : ''}${[
message.tKey,
'empty',
message.id,
'title'
]
.filter(e => e)
.join('.')}`
)
: message.title}
</Text>
{(() => {
@@ -122,16 +124,18 @@ export function EmptyStateScreen({
}}
whiteSpace="pre-wrap"
>
{t(
`${message.namespace ? `${message.namespace}:` : ''}${[
message.tKey,
'empty',
message.id,
'description'
]
.filter(e => e)
.join('.')}`
)}
{message.namespace === false
? ''
: t(
`${message.namespace ? `${message.namespace}:` : ''}${[
message.tKey,
'empty',
message.id,
'description'
]
.filter(e => e)
.join('.')}`
)}
</Text>
)
})()}

View File

@@ -9,7 +9,7 @@ import { ModalHeader } from '@/components/overlays'
import { Flex, Stack } from '@/components/primitives'
import { toast } from '@/providers'
const NamespaceContext = createContext<string | undefined>(undefined)
const NamespaceContext = createContext<string | false | undefined>(undefined)
export function useNamespace() {
return useContext(NamespaceContext)
@@ -50,7 +50,7 @@ export function FormModal<T extends FieldValues>({
title: string | React.ReactNode
icon: string
onClose: () => void
namespace?: string
namespace?: string | false
loading?: boolean
headerActions?: React.ReactNode
}
@@ -82,7 +82,7 @@ export function FormModal<T extends FieldValues>({
<ModalHeader
headerActions={headerActions}
icon={icon}
namespace={namespace ? namespace : undefined}
namespace={namespace}
title={title}
onClose={onClose}
/>

View File

@@ -18,7 +18,7 @@ type CheckboxFieldProps<TFieldValues extends FieldValues> = {
label: string
icon: string
disabled?: boolean
namespace?: string
namespace?: string | false
}
export function CheckboxField<TFieldValues extends FieldValues>({
@@ -38,12 +38,17 @@ export function CheckboxField<TFieldValues extends FieldValues>({
const activeNamespace = namespace ?? contextNamespace
const { t } = useTranslation(activeNamespace)
const { t } = useTranslation(
activeNamespace === false ? undefined : activeNamespace
)
const labelText = t([
['inputs', _.camelCase(label), 'label'].filter(Boolean).join('.'),
['inputs', _.camelCase(label)].filter(Boolean).join('.')
])
const labelText =
activeNamespace === false
? label
: t([
['inputs', _.camelCase(label), 'label'].filter(Boolean).join('.'),
['inputs', _.camelCase(label)].filter(Boolean).join('.')
])
function handleSwitchChange() {
field.onChange(!field.value)

View File

@@ -21,7 +21,7 @@ type FileFieldProps<TFieldValues extends FieldValues> = {
reminderText?: string
onImageRemoved?: () => void
required?: boolean
namespace?: string
namespace?: string | false
disabled?: boolean
sources?: FilePickerSourceConfig
mimeTypes?: Record<string, string[]>

View File

@@ -25,7 +25,7 @@ type ListboxFieldProps<TFieldValues extends FieldValues, TOption> = {
label: string
multiple?: boolean
disabled?: boolean
namespace?: string
namespace?: string | false
required?: boolean
options: ListboxOptionType<TOption>[]
actionButtonOption?: {

View File

@@ -17,7 +17,7 @@ type LocationFieldProps<TFieldValues extends FieldValues> = {
required?: boolean
disabled?: boolean
autoFocus?: boolean
namespace?: string
namespace?: string | false
}
export function LocationField<TFieldValues extends FieldValues>({

View File

@@ -12,7 +12,7 @@ import { useNamespace } from '../FormModal'
type SliderFieldProps<TFieldValues extends FieldValues> = {
control: Control<TFieldValues>
name: FieldPathByValue<TFieldValues, number | null | undefined>
namespace?: string
namespace?: string | false
} & Omit<SliderInputProps, 'value' | 'onChange'>
export function SliderField<TFieldValues extends FieldValues>({

View File

@@ -39,7 +39,7 @@ type ButtonOwnProps = {
/** Whether the button should be styled as dangerous/destructive with a red background. */
dangerous?: boolean
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Additional properties for the translation function. Used for dynamic translations. See the [i18n documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
tProps?: Record<string, unknown>
style?: CSSProperties
@@ -84,7 +84,7 @@ export function Button<T extends ElementType = 'button'>({
props: props as any
})
const { t } = useModuleTranslation([namespace])
const { t } = useModuleTranslation(namespace === false ? [] : [namespace])
return (
<Transition>
@@ -124,18 +124,22 @@ export function Button<T extends ElementType = 'button'>({
{children && typeof children === 'string' ? (
<Box asChild minWidth="0">
<Text truncate>
{t(
[
`buttons.${_.camelCase(children)}`,
`${_.camelCase(children)}`,
children,
`${namespace}:buttons.${_.camelCase(children)}`,
`${namespace}:${_.camelCase(children)}`,
`${namespace}:${children}`,
`common.buttons:${_.camelCase(children)}`
],
{ ...tProps, defaultValue: children }
)}
{namespace === false
? children
: t(
[
`buttons.${_.camelCase(children)}`,
`${_.camelCase(children)}`,
children,
`${namespace}:buttons.${_.camelCase(children)}`,
`${namespace}:${_.camelCase(children)}`,
`${namespace}:${children}`,
`common.buttons:buttons.${_.camelCase(children)}`,
`common.buttons:${_.camelCase(children)}`,
`common.buttons:${children}`
],
{ ...tProps, defaultValue: children }
)}
</Text>
</Box>
) : (

View File

@@ -30,7 +30,7 @@ export interface ColorInputProps {
/** Additional CSS class names to apply to the color input component. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
}

View File

@@ -42,7 +42,7 @@ interface ComboboxInputProps<T> {
/** Additional CSS class names to apply to the combobox. Use `!` suffix for Tailwind CSS class overrides. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
}

View File

@@ -33,7 +33,7 @@ export type CurrencyInputProps = {
/** Additional CSS class names to apply to the currency input component. */
className?: string
/** The i18n namespace for internationalization. Use false to disable translation. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
} & InputVariants

View File

@@ -42,7 +42,7 @@ export interface DateInputProps {
/** Whether the date input includes time selection. */
hasTime?: boolean
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
}

View File

@@ -89,7 +89,7 @@ export function FileInput({
onChange: (value: FileValue) => void
onImageRemoved?: () => void
required?: boolean
namespace?: string
namespace?: string | false
disabled?: boolean
sources?: FilePickerSourceConfig
mimeTypes?: Record<string, string[]>

View File

@@ -22,7 +22,7 @@ export interface IconInputProps {
required?: boolean
disabled?: boolean
autoFocus?: boolean
namespace?: string
namespace?: string | false
errorMsg?: string
}

View File

@@ -36,7 +36,7 @@ interface ListboxInputProps<T> {
/** The custom content to display in the listbox button. */
renderContent?: (value: T) => React.ReactNode
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** The error message to display when the field is invalid. */
errorMsg?: string
}

View File

@@ -27,7 +27,7 @@ interface LocationInputProps {
disabled?: boolean
autoFocus?: boolean
className?: string
namespace?: string
namespace?: string | false
errorMsg?: string
}

View File

@@ -23,7 +23,7 @@ export interface NumberInputProps {
/** Additional CSS class names to apply to the number input component. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
/** The minimum value allowed. */

View File

@@ -33,7 +33,7 @@ interface SearchInputProps extends Omit<FlexProps<'search'>, 'onChange'> {
/** Additional CSS class names to apply to the search input container. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Optional debounce delay in milliseconds. When set, the onChange callback will be debounced by this amount. */
debounceMs?: number
/** Policy for showing children components.
@@ -71,10 +71,11 @@ export function SearchInput({
children,
...props
}: SearchInputProps) {
const { t } = useModuleTranslation([
'common.misc',
...(namespace ? [namespace] : [])
])
const { t } = useModuleTranslation(
namespace === false
? []
: ['common.misc', ...(namespace ? [namespace] : [])]
)
// Internal state for immediate input feedback when debouncing
const [internalValue, setInternalValue] = useState(value)
@@ -214,27 +215,31 @@ export function SearchInput({
data-form-type="other"
data-lpignore="true"
disabled={disabled}
placeholder={t([`common.misc:search`, `Search ${searchTarget}`], {
item: t(
[
`items.${_.camelCase(searchTarget)}`,
`items.${searchTarget}`,
`${_.camelCase(searchTarget)}`,
`${searchTarget}`,
`${namespace}:items.${_.camelCase(searchTarget)}`,
`${namespace}:items.${searchTarget}`,
`${namespace}:${_.camelCase(searchTarget)}`,
`${namespace}:${searchTarget}`,
`common.misc:items.${_.camelCase(searchTarget)}`,
`common.misc:items.${searchTarget}`,
`common.misc:${_.camelCase(searchTarget)}`,
`common.misc:${searchTarget}`
],
{
defaultValue: searchTarget
}
)
})}
placeholder={
namespace === false
? `Search ${searchTarget}`
: t([`common.misc:search`, `Search ${searchTarget}`], {
item: t(
[
`items.${_.camelCase(searchTarget)}`,
`items.${searchTarget}`,
`${_.camelCase(searchTarget)}`,
`${searchTarget}`,
`${namespace}:items.${_.camelCase(searchTarget)}`,
`${namespace}:items.${searchTarget}`,
`${namespace}:${_.camelCase(searchTarget)}`,
`${namespace}:${searchTarget}`,
`common.misc:items.${_.camelCase(searchTarget)}`,
`common.misc:items.${searchTarget}`,
`common.misc:${_.camelCase(searchTarget)}`,
`common.misc:${searchTarget}`
],
{
defaultValue: searchTarget
}
)
})
}
style={{ paddingRight: actionButtonProps ? '5rem' : '2.5rem' }}
type="text"
value={displayValue}

View File

@@ -11,7 +11,7 @@ export function SliderHeader({
}: {
icon?: string
label?: string
namespace?: string
namespace?: string | false
value: number
required?: boolean
max?: number

View File

@@ -20,7 +20,7 @@ export interface SliderInputProps extends Omit<
max?: number
step?: number
className?: string
namespace?: string
namespace?: string | false
}
export function SliderInput({

View File

@@ -50,7 +50,7 @@ interface TagsInputProps {
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>
}
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
}

View File

@@ -31,7 +31,7 @@ export type TextAreaInputProps = {
/** Additional CSS class names to apply to the textarea element. Use `!` suffix for Tailwind CSS class overrides. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Error message to display when the input is invalid. */
errorMsg?: string
} & InputVariants

View File

@@ -34,7 +34,7 @@ export type TextInputProps = {
| 'search'
actionButtonProps?: React.ComponentProps<typeof Button>
className?: string
namespace?: string
namespace?: string | false
errorMsg?: string
inputRef?: React.RefObject<HTMLInputElement | null>
} & Omit<React.HTMLAttributes<HTMLInputElement>, 'onChange'> &

View File

@@ -6,11 +6,15 @@ export function useInputLabel({
namespace,
label
}: {
namespace?: string
namespace?: string | false
label: string
}) {
const { t } = useModuleTranslation(namespace ? [namespace] : undefined)
if (namespace === false) {
return label
}
return t(
[
`inputs.${_.camelCase(label)}.label`,

View File

@@ -23,7 +23,7 @@ interface ModuleHeaderProps {
contextMenuProps?: React.ComponentProps<typeof ContextMenu>
actionButton?: React.ReactNode
customElement?: React.ReactNode
namespace?: string
namespace?: string | false
tKey?: string
}
@@ -43,11 +43,11 @@ export function ModuleHeader({
title = title ?? innerTitle
icon = icon ?? innerIcon
const { t } = useModuleTranslation([
`common.${title}`,
'common.misc',
namespace ?? ''
])
const { t } = useModuleTranslation(
namespace === false
? []
: [`common.${title}`, 'common.misc', namespace ?? '']
)
const { toggleSidebar, sidebarExpanded } = useMainSidebarState()
@@ -105,15 +105,17 @@ export function ModuleHeader({
width="100%"
>
<Text truncate display="block">
{t([
`${namespace}:${tKey}.${title}.title`,
`${namespace}:${title}.title`,
`apps.${title}:title`,
`common.${title}:title`,
'common.misc:title',
'title',
title?.toString() ?? ''
])}
{namespace === false
? (title?.toString() ?? '')
: t([
`${namespace}:${tKey}.${title}.title`,
`${namespace}:${title}.title`,
`apps.${title}:title`,
`common.${title}:title`,
'common.misc:title',
'title',
title?.toString() ?? ''
])}
</Text>
<Box asChild minWidth="0">
<Text
@@ -135,15 +137,17 @@ export function ModuleHeader({
size={{ base: 'sm', sm: 'base' }}
whiteSpace="nowrap"
>
{t([
`${namespace}:${tKey}.${title}.description`,
`${namespace}:${title}.description`,
`apps.${title}:description`,
`common.${title}:description`,
'common.misc:description',
'description',
`Description for ${title?.toString() ?? ''}`
])}
{namespace === false
? `Description for ${title?.toString() ?? ''}`
: t([
`${namespace}:${tKey}.${title}.description`,
`${namespace}:${title}.description`,
`apps.${title}:description`,
`common.${title}:description`,
'common.misc:description',
'description',
`Description for ${title?.toString() ?? ''}`
])}
</Text>
</Box>
</Flex>

View File

@@ -57,11 +57,13 @@ export function SidebarTitle({
weight="semibold"
whiteSpace="nowrap"
>
{t([
`sidebar.${_.camelCase(label)}`,
`common.sidebar:categories.${_.camelCase(label)}`,
label
])}
{namespace === false
? label
: t([
`sidebar.${_.camelCase(label)}`,
`common.sidebar:categories.${_.camelCase(label)}`,
label
])}
</Text>
{actionButton && 'icon' in actionButton ? (
<Text

View File

@@ -44,7 +44,7 @@ interface ContextMenuItemProps {
/** Additional CSS class names to apply to the menu item. */
className?: string
/** The i18n namespace for internationalization. See the [main documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
namespace?: string
namespace?: string | false
/** Additional properties for the translation function. Used for dynamic translations. See the [i18n documentation](https://docs.lifeforge.melvinchia.dev) for more details. */
tProps?: Record<string, unknown>
/** Callback function called when the menu item is clicked. */
@@ -64,7 +64,9 @@ export function ContextMenuItem({
tProps,
onClick
}: ContextMenuItemProps) {
const { t } = useModuleTranslation(namespace ? [namespace] : undefined)
const { t } = useModuleTranslation(
namespace ? [namespace] : undefined
)
return (
<WithDivide>
@@ -130,7 +132,10 @@ export function ContextMenuItem({
[
_.camelCase(label),
`buttons.${_.camelCase(label)}`,
label
label,
`${namespace}:${_.camelCase(label)}`,
`${namespace}:buttons.${_.camelCase(label)}`,
`${namespace}:${label}`
],
tProps
)

View File

@@ -16,7 +16,7 @@ function getLocaleKeys(innerTitle: string, namespace?: string) {
`${innerTitle}.title`,
`${innerTitle}`,
`modals.${innerTitle}.title`,
`modals.${innerTitle}`,
`modals.${innerTitle}`
].map(e => (namespace ? `${namespace}:${e}` : e))
}
@@ -36,7 +36,7 @@ function _ModalHeader({
hasAI?: boolean
className?: string
appendTitle?: React.ReactElement
namespace?: string
namespace?: string | false
headerActions?: React.ReactNode
}) {
const { t } = useModuleTranslation(namespace ? [namespace] : [])
@@ -65,16 +65,20 @@ function _ModalHeader({
{typeof innerTitle === 'string' ? (
<>
<Text truncate as="span" style={{ minWidth: 0 }}>
{t(
[
...getLocaleKeys(innerTitle),
...(namespace ? getLocaleKeys(innerTitle, namespace) : []),
...getLocaleKeys(innerTitle, 'common.modals')
],
{
defaultValue: innerTitle
}
)}
{namespace === false
? innerTitle
: t(
[
...getLocaleKeys(innerTitle),
...(namespace
? getLocaleKeys(innerTitle, namespace)
: []),
...getLocaleKeys(innerTitle, 'common.modals')
],
{
defaultValue: innerTitle
}
)}
</Text>
{appendTitle}
{hasAI && (