GenCN UI

GenUI I18n Text

GenUI-powered i18n text component with automatic language detection and translation to your target language.

Usage

Loading preview...

Installation

npx shadcn@latest add https://gencn-ui.encatch.com/r/genui-i18n-text.json
"use client";
import * as React from "react";
import { Languages } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { translateOnce } from "@/registry/new-york/gencn-ui/items/shared/lib/translator";
import { detectLanguages } from "@/registry/new-york/gencn-ui/items/shared/lib/language-detector";
import { Button } from "@/components/ui/button";

export type I18nTextShowOption = "inline-auto" | "inline-manual" | "on-hover";

export interface GenUII18nTextProps {
  text: string;
  language: string;
  showOption: I18nTextShowOption;
}

export function GenUII18nText({ text, language, showOption }: GenUII18nTextProps) {
  const [translatedText, setTranslatedText] = React.useState<string | null>(null);
  const [isShowingTranslated, setIsShowingTranslated] = React.useState<boolean>(showOption === "inline-auto");
  const [isLoading, setIsLoading] = React.useState<boolean>(false);
  const [error, setError] = React.useState<string | null>(null);
  const abortRef = React.useRef<AbortController | null>(null);

  React.useEffect(() => {
    setTranslatedText(null);
    setIsShowingTranslated(showOption === "inline-auto");
    setError(null);
    if (abortRef.current) {
      abortRef.current.abort();
      abortRef.current = null;
    }
  }, [text, language, showOption]);

  const ensureTranslation = React.useCallback(async () => {
    if (translatedText !== null) return translatedText;
    setIsLoading(true);
    setError(null);
    const controller = new AbortController();
    abortRef.current = controller;
    try {
      let sourceLanguage: string | undefined = undefined;
      try {
        const detected = await detectLanguages(text);
        if (Array.isArray(detected) && detected.length > 0) {
          sourceLanguage = (detected[0] as any).detectedLanguage as string;
        }
      } catch {}
      const result = await translateOnce(text, { sourceLanguage, targetLanguage: language, signal: controller.signal });
      // Only set the result if the signal wasn't aborted
      if (!controller.signal.aborted) {
        setTranslatedText(result);
        return result;
      }
      return null;
    } catch (e) {
      const msg = (e as Error).message || "Failed to translate";
      // Don't set error for aborted translations - this is expected behavior
      if (!msg.includes('aborted') && !controller.signal.aborted) {
        setError(msg);
      }
      return null;
    } finally {
      setIsLoading(false);
      abortRef.current = null;
    }
  }, [text, language, translatedText]);

  React.useEffect(() => {
    if (showOption === "inline-auto") {
      void ensureTranslation().then((res) => {
        if (res !== null) setIsShowingTranslated(true);
      });
    }
  }, [showOption, ensureTranslation]);

  if (showOption === "on-hover") {
    return (
      <Tooltip onOpenChange={(open) => { if (open) void ensureTranslation(); }}>
        <TooltipTrigger asChild>
          <span className="inline-flex items-center cursor-help underline decoration-dashed decoration-1 underline-offset-4">{text}</span>
        </TooltipTrigger>
        <TooltipContent>
          {/* {error ? `Error: ${error}` : (translatedText ?? (isLoading ? "Translating…" : ""))} */}
          {translatedText ?? (isLoading ? "Translating…" : "")}
        </TooltipContent>
      </Tooltip>
    );
  }

  const content = isShowingTranslated ? (translatedText ?? (isLoading ? "Translating…" : text)) : text;

  return (
    <span className="inline-flex items-center">
      <span>{content}</span>
      {showOption === "inline-manual" && (
        <Button
        variant="outline"
          aria-label={isShowingTranslated ? "Show original" : "Translate"}
          className="ml-2 inline-flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground text-xs shadow hover:opacity-90 disabled:opacity-60"
          onClick={async () => {
            if (!isShowingTranslated) {
              const res = await ensureTranslation();
              if (res !== null) setIsShowingTranslated(true);
            } else {
              setIsShowingTranslated(false);
            }
          }}
          disabled={isLoading && !isShowingTranslated}
        >
          <Languages className="h-4 w-4" />
        </Button>
      )}
      {/* {error && <span className="ml-2 text-xs text-destructive">{error}</span>} */}
    </span>
  );
}


Props

Prop

Type

Dependencies

  • react
  • genui-provider
  • tooltip
  • button