feat(i18n): Enhance RTL support and update language direction handling (#698)
* feat(i18n): Enhance RTL support and update language direction handling * format * minor tweaks --------- Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
7ee324a347
commit
804c1263d5
16 changed files with 124 additions and 27 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
|
||||||
14
src/App.tsx
14
src/App.tsx
|
|
@ -1,5 +1,6 @@
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef } from "react";
|
||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { platform } from "@tauri-apps/plugin-os";
|
import { platform } from "@tauri-apps/plugin-os";
|
||||||
import {
|
import {
|
||||||
checkAccessibilityPermission,
|
checkAccessibilityPermission,
|
||||||
|
|
@ -13,6 +14,7 @@ import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar";
|
||||||
import { useSettings } from "./hooks/useSettings";
|
import { useSettings } from "./hooks/useSettings";
|
||||||
import { useSettingsStore } from "./stores/settingsStore";
|
import { useSettingsStore } from "./stores/settingsStore";
|
||||||
import { commands } from "@/bindings";
|
import { commands } from "@/bindings";
|
||||||
|
import { getLanguageDirection, initializeRTL } from "@/lib/utils/rtl";
|
||||||
|
|
||||||
type OnboardingStep = "accessibility" | "model" | "done";
|
type OnboardingStep = "accessibility" | "model" | "done";
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ const renderSettingsContent = (section: SidebarSection) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
|
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
@ -32,6 +35,7 @@ function App() {
|
||||||
const [currentSection, setCurrentSection] =
|
const [currentSection, setCurrentSection] =
|
||||||
useState<SidebarSection>("general");
|
useState<SidebarSection>("general");
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
|
const direction = getLanguageDirection(i18n.language);
|
||||||
const refreshAudioDevices = useSettingsStore(
|
const refreshAudioDevices = useSettingsStore(
|
||||||
(state) => state.refreshAudioDevices,
|
(state) => state.refreshAudioDevices,
|
||||||
);
|
);
|
||||||
|
|
@ -44,6 +48,11 @@ function App() {
|
||||||
checkOnboardingStatus();
|
checkOnboardingStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Initialize RTL direction when language changes
|
||||||
|
useEffect(() => {
|
||||||
|
initializeRTL(i18n.language);
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
// Initialize Enigo, shortcuts, and refresh audio devices when main app loads
|
// Initialize Enigo, shortcuts, and refresh audio devices when main app loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
|
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
|
||||||
|
|
@ -146,7 +155,10 @@ function App() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col select-none cursor-default">
|
<div
|
||||||
|
dir={direction}
|
||||||
|
className="h-screen flex flex-col select-none cursor-default"
|
||||||
|
>
|
||||||
<Toaster
|
<Toaster
|
||||||
theme="system"
|
theme="system"
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||||
.map(([id, config]) => ({ id: id as SidebarSection, ...config }));
|
.map(([id, config]) => ({ id: id as SidebarSection, ...config }));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-40 h-full border-r border-mid-gray/20 items-center px-2">
|
<div className="flex flex-col w-40 h-full border-e border-mid-gray/20 items-center px-2">
|
||||||
<HandyTextLogo width={120} className="m-4" />
|
<HandyTextLogo width={120} className="m-4" />
|
||||||
<div className="flex flex-col w-full items-center gap-1 pt-2 border-t border-mid-gray/20">
|
<div className="flex flex-col w-full items-center gap-1 pt-2 border-t border-mid-gray/20">
|
||||||
{availableSections.map((section) => {
|
{availableSections.map((section) => {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-full left-0 mb-2 w-64 bg-background border border-mid-gray/20 rounded-lg shadow-lg py-2 z-50">
|
<div className="absolute bottom-full start-0 mb-2 w-64 bg-background border border-mid-gray/20 rounded-lg shadow-lg py-2 z-50">
|
||||||
{/* First Run Welcome */}
|
{/* First Run Welcome */}
|
||||||
{isFirstRun && (
|
{isFirstRun && (
|
||||||
<div className="px-3 py-2 bg-logo-primary/10 border-b border-logo-primary/20">
|
<div className="px-3 py-2 bg-logo-primary/10 border-b border-logo-primary/20">
|
||||||
|
|
@ -97,7 +97,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||||
}}
|
}}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
className={`w-full px-3 py-2 text-left hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${
|
className={`w-full px-3 py-2 text-start hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${
|
||||||
currentModelId === model.id
|
currentModelId === model.id
|
||||||
? "bg-logo-primary/10 text-logo-primary"
|
? "bg-logo-primary/10 text-logo-primary"
|
||||||
: ""
|
: ""
|
||||||
|
|
@ -108,7 +108,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{getTranslatedModelName(model, t)}
|
{getTranslatedModelName(model, t)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-text/40 italic pr-4">
|
<div className="text-xs text-text/40 italic pe-4">
|
||||||
{getTranslatedModelDescription(model, t)}
|
{getTranslatedModelDescription(model, t)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -174,7 +174,7 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
aria-disabled={isDownloading}
|
aria-disabled={isDownloading}
|
||||||
className={`w-full px-3 py-2 text-left hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${
|
className={`w-full px-3 py-2 text-start hover:bg-mid-gray/10 transition-colors cursor-pointer focus:outline-none ${
|
||||||
isDownloading
|
isDownloading
|
||||||
? "opacity-50 cursor-not-allowed hover:bg-transparent"
|
? "opacity-50 cursor-not-allowed hover:bg-transparent"
|
||||||
: ""
|
: ""
|
||||||
|
|
@ -185,12 +185,12 @@ const ModelDropdown: React.FC<ModelDropdownProps> = ({
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{getTranslatedModelName(model, t)}
|
{getTranslatedModelName(model, t)}
|
||||||
{model.id === "parakeet-tdt-0.6b-v3" && isFirstRun && (
|
{model.id === "parakeet-tdt-0.6b-v3" && isFirstRun && (
|
||||||
<span className="ml-2 text-xs bg-logo-primary/20 text-logo-primary px-1.5 py-0.5 rounded">
|
<span className="ms-2 text-xs bg-logo-primary/20 text-logo-primary px-1.5 py-0.5 rounded">
|
||||||
{t("onboarding.recommended")}
|
{t("onboarding.recommended")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-text/40 italic pr-4">
|
<div className="text-xs text-text/40 italic pe-4">
|
||||||
{getTranslatedModelDescription(model, t)}
|
{getTranslatedModelDescription(model, t)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-text/50 tabular-nums">
|
<div className="mt-1 text-xs text-text/50 tabular-nums">
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
||||||
const displayDescription = getTranslatedModelDescription(model, t);
|
const displayDescription = getTranslatedModelDescription(model, t);
|
||||||
|
|
||||||
const baseButtonClasses =
|
const baseButtonClasses =
|
||||||
"flex justify-between items-center rounded-xl p-3 px-4 text-left transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] cursor-pointer group";
|
"flex justify-between items-center rounded-xl p-3 px-4 text-start transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-logo-primary/25 active:scale-[0.98] cursor-pointer group";
|
||||||
|
|
||||||
const variantClasses = isFeatured
|
const variantClasses = isFeatured
|
||||||
? "border-2 border-logo-primary/25 bg-logo-primary/5 hover:border-logo-primary/40 hover:bg-logo-primary/8 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-logo-primary/25 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100"
|
? "border-2 border-logo-primary/25 bg-logo-primary/5 hover:border-logo-primary/40 hover:bg-logo-primary/8 hover:shadow-lg hover:scale-[1.02] disabled:hover:border-logo-primary/25 disabled:hover:bg-logo-primary/5 disabled:hover:shadow-none disabled:hover:scale-100"
|
||||||
|
|
@ -64,7 +64,7 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-text/70 w-16 text-right">
|
<p className="text-xs text-text/70 w-16 text-end">
|
||||||
{t("onboarding.modelCard.accuracy")}
|
{t("onboarding.modelCard.accuracy")}
|
||||||
</p>
|
</p>
|
||||||
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
||||||
|
|
@ -75,7 +75,7 @@ const ModelCard: React.FC<ModelCardProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<p className="text-xs text-text/70 w-16 text-right">
|
<p className="text-xs text-text/70 w-16 text-end">
|
||||||
{t("onboarding.modelCard.speed")}
|
{t("onboarding.modelCard.speed")}
|
||||||
</p>
|
</p>
|
||||||
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
<div className="w-20 h-2 bg-mid-gray/20 rounded-full overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div className="relative" ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-left flex items-center justify-between transition-all duration-150 ${
|
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-start flex items-center justify-between transition-all duration-150 ${
|
||||||
isUpdating("selected_language")
|
isUpdating("selected_language")
|
||||||
? "opacity-50 cursor-not-allowed"
|
? "opacity-50 cursor-not-allowed"
|
||||||
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
||||||
|
|
@ -108,7 +108,7 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedLanguageName}</span>
|
<span className="truncate">{selectedLanguageName}</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 ml-2 transition-transform duration-200 ${
|
className={`w-4 h-4 ms-2 transition-transform duration-200 ${
|
||||||
isOpen ? "transform rotate-180" : ""
|
isOpen ? "transform rotate-180" : ""
|
||||||
}`}
|
}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|
@ -149,7 +149,7 @@ export const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
||||||
<button
|
<button
|
||||||
key={language.value}
|
key={language.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
className={`w-full px-2 py-1 text-sm text-start hover:bg-logo-primary/10 transition-colors duration-150 ${
|
||||||
selectedLanguage === language.value
|
selectedLanguage === language.value
|
||||||
? "bg-logo-primary/20 text-logo-primary font-semibold"
|
? "bg-logo-primary/20 text-logo-primary font-semibold"
|
||||||
: ""
|
: ""
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ const ProgressBar: React.FC<ProgressBarProps> = ({
|
||||||
{(showSpeed || showLabel) && (
|
{(showSpeed || showLabel) && (
|
||||||
<div className="text-xs text-text/60 tabular-nums min-w-fit">
|
<div className="text-xs text-text/60 tabular-nums min-w-fit">
|
||||||
{showLabel && item.label && (
|
{showLabel && item.label && (
|
||||||
<span className="mr-2">{item.label}</span>
|
<span className="me-2">{item.label}</span>
|
||||||
)}
|
)}
|
||||||
{showSpeed && item.speed !== undefined && item.speed > 0 ? (
|
{showSpeed && item.speed !== undefined && item.speed > 0 ? (
|
||||||
// eslint-disable-next-line i18next/no-literal-string
|
// eslint-disable-next-line i18next/no-literal-string
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
<div className={`relative ${className}`} ref={dropdownRef}>
|
<div className={`relative ${className}`} ref={dropdownRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-left flex items-center justify-between transition-all duration-150 ${
|
className={`px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded min-w-[200px] text-start flex items-center justify-between transition-all duration-150 ${
|
||||||
disabled
|
disabled
|
||||||
? "opacity-50 cursor-not-allowed"
|
? "opacity-50 cursor-not-allowed"
|
||||||
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
: "hover:bg-logo-primary/10 cursor-pointer hover:border-logo-primary"
|
||||||
|
|
@ -72,7 +72,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
>
|
>
|
||||||
<span className="truncate">{selectedOption?.label || placeholder}</span>
|
<span className="truncate">{selectedOption?.label || placeholder}</span>
|
||||||
<svg
|
<svg
|
||||||
className={`w-4 h-4 ml-2 transition-transform duration-200 ${isOpen ? "transform rotate-180" : ""}`}
|
className={`w-4 h-4 ms-2 transition-transform duration-200 ${isOpen ? "transform rotate-180" : ""}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|
@ -96,7 +96,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
|
||||||
<button
|
<button
|
||||||
key={option.value}
|
key={option.value}
|
||||||
type="button"
|
type="button"
|
||||||
className={`w-full px-2 py-1 text-sm text-left hover:bg-logo-primary/10 transition-colors duration-150 ${
|
className={`w-full px-2 py-1 text-sm text-start hover:bg-logo-primary/10 transition-colors duration-150 ${
|
||||||
selectedValue === option.value
|
selectedValue === option.value
|
||||||
? "bg-logo-primary/20 font-semibold"
|
? "bg-logo-primary/20 font-semibold"
|
||||||
: ""
|
: ""
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const Input: React.FC<InputProps> = ({
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left transition-all duration-150";
|
"px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-start transition-all duration-150";
|
||||||
|
|
||||||
const interactiveClasses = disabled
|
const interactiveClasses = disabled
|
||||||
? "opacity-60 cursor-not-allowed bg-mid-gray/10 border-mid-gray/40"
|
? "opacity-60 cursor-not-allowed bg-mid-gray/10 border-mid-gray/40"
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export const Slider: React.FC<SliderProps> = ({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{showValue && (
|
{showValue && (
|
||||||
<span className="text-sm font-medium text-text/90 min-w-10 text-right">
|
<span className="text-sm font-medium text-text/90 min-w-10 text-end">
|
||||||
{formatValue(value)}
|
{formatValue(value)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const Textarea: React.FC<TextareaProps> = ({
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
"px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-left transition-[background-color,border-color] duration-150 hover:bg-logo-primary/10 hover:border-logo-primary focus:outline-none focus:bg-logo-primary/10 focus:border-logo-primary resize-y";
|
"px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 rounded text-start transition-[background-color,border-color] duration-150 hover:bg-logo-primary/10 hover:border-logo-primary focus:outline-none focus:bg-logo-primary/10 focus:border-logo-primary resize-y";
|
||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: "px-3 py-2 min-h-[100px]",
|
default: "px-3 py-2 min-h-[100px]",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,11 @@ import { initReactI18next } from "react-i18next";
|
||||||
import { locale } from "@tauri-apps/plugin-os";
|
import { locale } from "@tauri-apps/plugin-os";
|
||||||
import { LANGUAGE_METADATA } from "./languages";
|
import { LANGUAGE_METADATA } from "./languages";
|
||||||
import { commands } from "@/bindings";
|
import { commands } from "@/bindings";
|
||||||
|
import {
|
||||||
|
getLanguageDirection,
|
||||||
|
updateDocumentDirection,
|
||||||
|
updateDocumentLanguage,
|
||||||
|
} from "@/lib/utils/rtl";
|
||||||
|
|
||||||
// Auto-discover translation files using Vite's glob import
|
// Auto-discover translation files using Vite's glob import
|
||||||
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>(
|
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>(
|
||||||
|
|
@ -95,4 +100,14 @@ export const syncLanguageFromSettings = async () => {
|
||||||
// Run language sync on init
|
// Run language sync on init
|
||||||
syncLanguageFromSettings();
|
syncLanguageFromSettings();
|
||||||
|
|
||||||
|
// Listen for language changes to update HTML dir and lang attributes
|
||||||
|
i18n.on("languageChanged", (lng) => {
|
||||||
|
const dir = getLanguageDirection(lng);
|
||||||
|
updateDocumentDirection(dir);
|
||||||
|
updateDocumentLanguage(lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-export RTL utilities for convenience
|
||||||
|
export { getLanguageDirection, isRTLLanguage } from "@/lib/utils/rtl";
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,16 @@
|
||||||
* 1. Create a new folder: src/i18n/locales/{code}/translation.json
|
* 1. Create a new folder: src/i18n/locales/{code}/translation.json
|
||||||
* 2. Add an entry here with the language code, English name, and native name
|
* 2. Add an entry here with the language code, English name, and native name
|
||||||
* 3. Optionally add a priority (lower = higher in dropdown, no priority = alphabetical at end)
|
* 3. Optionally add a priority (lower = higher in dropdown, no priority = alphabetical at end)
|
||||||
|
* 4. For RTL languages, add direction: 'rtl'
|
||||||
*/
|
*/
|
||||||
export const LANGUAGE_METADATA: Record<
|
export const LANGUAGE_METADATA: Record<
|
||||||
string,
|
string,
|
||||||
{ name: string; nativeName: string; priority?: number }
|
{
|
||||||
|
name: string;
|
||||||
|
nativeName: string;
|
||||||
|
priority?: number;
|
||||||
|
direction?: "ltr" | "rtl";
|
||||||
|
}
|
||||||
> = {
|
> = {
|
||||||
en: { name: "English", nativeName: "English", priority: 1 },
|
en: { name: "English", nativeName: "English", priority: 1 },
|
||||||
zh: { name: "Chinese", nativeName: "中文", priority: 2 },
|
zh: { name: "Chinese", nativeName: "中文", priority: 2 },
|
||||||
|
|
@ -24,5 +30,5 @@ export const LANGUAGE_METADATA: Record<
|
||||||
pt: { name: "Portuguese", nativeName: "Português", priority: 12 },
|
pt: { name: "Portuguese", nativeName: "Português", priority: 12 },
|
||||||
cs: { name: "Czech", nativeName: "Čeština", priority: 13 },
|
cs: { name: "Czech", nativeName: "Čeština", priority: 13 },
|
||||||
tr: { name: "Turkish", nativeName: "Türkçe", priority: 14 },
|
tr: { name: "Turkish", nativeName: "Türkçe", priority: 14 },
|
||||||
ar: { name: "Arabic", nativeName: "العربية", priority: 15 },
|
ar: { name: "Arabic", nativeName: "العربية", priority: 15, direction: "rtl" },
|
||||||
};
|
};
|
||||||
|
|
|
||||||
59
src/lib/utils/rtl.ts
Normal file
59
src/lib/utils/rtl.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* RTL (Right-to-Left) utilities for handling text direction in the application.
|
||||||
|
*
|
||||||
|
* These utilities help manage RTL languages like Arabic, Hebrew, Persian, and Urdu.
|
||||||
|
* They work with the i18n system to automatically update HTML attributes when
|
||||||
|
* the language changes.
|
||||||
|
*/
|
||||||
|
import { LANGUAGE_METADATA } from "@/i18n/languages";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a language code is RTL (Right-to-Left)
|
||||||
|
* @param langCode - The language code (e.g., 'ar', 'en', 'he')
|
||||||
|
* @returns true if the language is RTL, false otherwise
|
||||||
|
*/
|
||||||
|
export const isRTLLanguage = (langCode: string): boolean => {
|
||||||
|
if (!langCode) return false;
|
||||||
|
const code = langCode.split("-")[0].toLowerCase();
|
||||||
|
return LANGUAGE_METADATA[code]?.direction === "rtl";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text direction ('ltr' or 'rtl') for a language
|
||||||
|
* @param langCode - The language code (e.g., 'ar', 'en', 'he')
|
||||||
|
* @returns 'rtl' if RTL language, 'ltr' otherwise
|
||||||
|
*/
|
||||||
|
export const getLanguageDirection = (langCode: string): "ltr" | "rtl" => {
|
||||||
|
return isRTLLanguage(langCode) ? "rtl" : "ltr";
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the HTML document's dir attribute
|
||||||
|
* @param dir - The direction ('ltr' or 'rtl')
|
||||||
|
*/
|
||||||
|
export const updateDocumentDirection = (dir: "ltr" | "rtl"): void => {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.setAttribute("dir", dir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the HTML document's lang attribute
|
||||||
|
* @param lang - The language code (e.g., 'ar', 'en')
|
||||||
|
*/
|
||||||
|
export const updateDocumentLanguage = (lang: string): void => {
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.setAttribute("lang", lang);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize RTL support for the current document
|
||||||
|
* Should be called when the app initializes and when language changes
|
||||||
|
* @param langCode - The current language code
|
||||||
|
*/
|
||||||
|
export const initializeRTL = (langCode: string): void => {
|
||||||
|
const dir = getLanguageDirection(langCode);
|
||||||
|
updateDocumentDirection(dir);
|
||||||
|
updateDocumentLanguage(langCode);
|
||||||
|
};
|
||||||
|
|
@ -8,7 +8,8 @@ import {
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import "./RecordingOverlay.css";
|
import "./RecordingOverlay.css";
|
||||||
import { commands } from "@/bindings";
|
import { commands } from "@/bindings";
|
||||||
import { syncLanguageFromSettings } from "@/i18n";
|
import i18n, { syncLanguageFromSettings } from "@/i18n";
|
||||||
|
import { getLanguageDirection } from "@/lib/utils/rtl";
|
||||||
|
|
||||||
type OverlayState = "recording" | "transcribing";
|
type OverlayState = "recording" | "transcribing";
|
||||||
|
|
||||||
|
|
@ -18,6 +19,7 @@ const RecordingOverlay: React.FC = () => {
|
||||||
const [state, setState] = useState<OverlayState>("recording");
|
const [state, setState] = useState<OverlayState>("recording");
|
||||||
const [levels, setLevels] = useState<number[]>(Array(16).fill(0));
|
const [levels, setLevels] = useState<number[]>(Array(16).fill(0));
|
||||||
const smoothedLevelsRef = useRef<number[]>(Array(16).fill(0));
|
const smoothedLevelsRef = useRef<number[]>(Array(16).fill(0));
|
||||||
|
const direction = getLanguageDirection(i18n.language);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const setupEventListeners = async () => {
|
const setupEventListeners = async () => {
|
||||||
|
|
@ -69,7 +71,10 @@ const RecordingOverlay: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`recording-overlay ${isVisible ? "fade-in" : ""}`}>
|
<div
|
||||||
|
dir={direction}
|
||||||
|
className={`recording-overlay ${isVisible ? "fade-in" : ""}`}
|
||||||
|
>
|
||||||
<div className="overlay-left">{getIcon()}</div>
|
<div className="overlay-left">{getIcon()}</div>
|
||||||
|
|
||||||
<div className="overlay-middle">
|
<div className="overlay-middle">
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" dir="ltr">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Recording Overlay</title>
|
<title>Recording Overlay</title>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue