feat(i18n): add language switcher with en/ja translations
- New client-side i18n via React Context (useI18n, tArray, I18nProvider) - Catalog ships 21 locale stubs; only zh-CN/en/ja have reviewed translations - Header language switcher (globe icon + short label) before settings gear - All hardcoded Chinese UI text migrated to keys: typewriter, options, hints (with embedded gear icon via dangerouslySetInnerHTML), settings panel, footer/about, play page hints - AI output language follows user-selected locale via trailing one-liner directive appended to Architect/Writer/CharacterDesigner/InsertBeat user messages (preserves system-prompt cacheability) - Per-locale separator rule: zh uses middot between every glyph; en/ja use plain spaces - Option value → i18n key suffix maps preserve Chinese as the underlying identifier so analytics unions and STYLE_MAP keys stay byte-stable Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import type { Locale } from "./config";
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
LOCALE_STORAGE_KEY,
|
||||
getInitialLocale,
|
||||
setLocale as saveLocale,
|
||||
} from "./config";
|
||||
import { getNestedValue, formatTranslation } from "./utils";
|
||||
|
||||
// Translation function type
|
||||
export type TranslationFunction = (
|
||||
key: string,
|
||||
params?: Record<string, string | number | boolean>,
|
||||
) => string;
|
||||
|
||||
// Context type
|
||||
interface I18nContextType {
|
||||
locale: Locale;
|
||||
setLocale: (locale: Locale) => void;
|
||||
t: TranslationFunction;
|
||||
// Returns an array of strings stored under the key (e.g. the typewriter
|
||||
// example phrases). Falls back to the key wrapped in an array so callers
|
||||
// can safely index.
|
||||
tArray: (key: string) => string[];
|
||||
}
|
||||
|
||||
const I18nContext = createContext<I18nContextType | undefined>(undefined);
|
||||
|
||||
// Provider props
|
||||
interface I18nProviderProps {
|
||||
children: ReactNode;
|
||||
initialLocale?: Locale;
|
||||
}
|
||||
|
||||
// Dynamic import of locale files
|
||||
async function importLocale(locale: Locale) {
|
||||
switch (locale) {
|
||||
case "zh-CN":
|
||||
return (await import("./locales/zh-CN")).zhCN;
|
||||
case "en":
|
||||
return (await import("./locales/en")).en;
|
||||
case "zh-TW":
|
||||
return (await import("./locales/zh-TW")).zhTW;
|
||||
case "zh-HK":
|
||||
return (await import("./locales/zh-HK")).zhHK;
|
||||
case "ja":
|
||||
return (await import("./locales/ja")).ja;
|
||||
case "ko":
|
||||
return (await import("./locales/ko")).ko;
|
||||
case "es":
|
||||
return (await import("./locales/es")).es;
|
||||
case "fr":
|
||||
return (await import("./locales/fr")).fr;
|
||||
case "de":
|
||||
return (await import("./locales/de")).de;
|
||||
case "pt-BR":
|
||||
return (await import("./locales/pt-BR")).ptBR;
|
||||
case "pt":
|
||||
return (await import("./locales/pt")).pt;
|
||||
case "ru":
|
||||
return (await import("./locales/ru")).ru;
|
||||
case "it":
|
||||
return (await import("./locales/it")).it;
|
||||
case "vi":
|
||||
return (await import("./locales/vi")).vi;
|
||||
case "th":
|
||||
return (await import("./locales/th")).th;
|
||||
case "id":
|
||||
return (await import("./locales/id")).id;
|
||||
case "tr":
|
||||
return (await import("./locales/tr")).tr;
|
||||
case "pl":
|
||||
return (await import("./locales/pl")).pl;
|
||||
case "nl":
|
||||
return (await import("./locales/nl")).nl;
|
||||
case "uk":
|
||||
return (await import("./locales/uk")).uk;
|
||||
case "hi":
|
||||
return (await import("./locales/hi")).hi;
|
||||
case "cs":
|
||||
return (await import("./locales/cs")).cs;
|
||||
default:
|
||||
console.warn(`Locale ${locale} not loaded, falling back to English`);
|
||||
return (await import("./locales/en")).en;
|
||||
}
|
||||
}
|
||||
|
||||
// Provider component
|
||||
export function I18nProvider({ children, initialLocale }: I18nProviderProps) {
|
||||
const [locale, setLocaleState] = useState<Locale>(initialLocale ?? DEFAULT_LOCALE);
|
||||
const [translations, setTranslations] = useState<Record<string, unknown>>({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load translations when locale changes
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
async function loadTranslations() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const localeData = await importLocale(locale);
|
||||
if (!cancelled) {
|
||||
setTranslations(localeData as Record<string, unknown>);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load translations for ${locale}:`, error);
|
||||
if (!cancelled) {
|
||||
// Fallback to default locale on error
|
||||
if (locale !== DEFAULT_LOCALE) {
|
||||
const fallback = await importLocale(DEFAULT_LOCALE);
|
||||
setTranslations(fallback as Record<string, unknown>);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadTranslations();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [locale]);
|
||||
|
||||
// Keep <html lang="..."> in sync with the active locale for a11y / SEO.
|
||||
useEffect(() => {
|
||||
if (typeof document !== "undefined") {
|
||||
document.documentElement.lang = locale;
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
// Set locale function
|
||||
const setLocale = (newLocale: Locale) => {
|
||||
saveLocale(newLocale);
|
||||
setLocaleState(newLocale);
|
||||
};
|
||||
|
||||
// Translation function
|
||||
const t: TranslationFunction = (key, params = {}) => {
|
||||
if (isLoading) {
|
||||
return key; // Return key during loading
|
||||
}
|
||||
|
||||
const value = getNestedValue(translations, key);
|
||||
|
||||
if (value === undefined) {
|
||||
console.warn(`Translation missing for key: ${key}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
if (typeof value === "function") {
|
||||
return (value as (params: Record<string, string | number | boolean>) => string)(params);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return formatTranslation(value, params);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const tArray: I18nContextType["tArray"] = (key) => {
|
||||
if (isLoading) return [];
|
||||
const value = getNestedValue(translations, key);
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => (typeof v === "string" ? v : String(v)));
|
||||
}
|
||||
if (value === undefined) {
|
||||
console.warn(`Translation array missing for key: ${key}`);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ locale, setLocale, t, tArray }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Hook to use i18n
|
||||
export function useI18n() {
|
||||
const context = useContext(I18nContext);
|
||||
if (!context) {
|
||||
throw new Error("useI18n must be used within I18nProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// Hook to get just the translation function (for server-side or non-provider contexts)
|
||||
export function useTranslation(locale?: Locale) {
|
||||
const { t: clientT, locale: currentLocale } = useI18n();
|
||||
|
||||
return {
|
||||
t: clientT,
|
||||
locale: locale ?? currentLocale,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Supported locales for InfiPlot
|
||||
export const DEFAULT_LOCALE = "zh-CN" as const;
|
||||
|
||||
export type Locale =
|
||||
| "en"
|
||||
| "zh-CN"
|
||||
| "zh-TW"
|
||||
| "zh-HK"
|
||||
| "ja"
|
||||
| "ko"
|
||||
| "es"
|
||||
| "fr"
|
||||
| "de"
|
||||
| "pt-BR"
|
||||
| "pt"
|
||||
| "ru"
|
||||
| "it"
|
||||
| "vi"
|
||||
| "th"
|
||||
| "id"
|
||||
| "tr"
|
||||
| "pl"
|
||||
| "nl"
|
||||
| "uk"
|
||||
| "hi"
|
||||
| "cs";
|
||||
|
||||
export const LOCALE_NAMES: Record<Locale, string> = {
|
||||
"en": "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文(台灣)",
|
||||
"zh-HK": "繁體中文(香港)",
|
||||
"ja": "日本語",
|
||||
"ko": "한국어",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
"de": "Deutsch",
|
||||
"pt-BR": "Português (Brasil)",
|
||||
"pt": "Português",
|
||||
"ru": "Русский",
|
||||
"it": "Italiano",
|
||||
"vi": "Tiếng Việt",
|
||||
"th": "ภาษาไทย",
|
||||
"id": "Bahasa Indonesia",
|
||||
"tr": "Türkçe",
|
||||
"pl": "Polski",
|
||||
"nl": "Nederlands",
|
||||
"uk": "Українська",
|
||||
"hi": "हिन्दी",
|
||||
"cs": "Čeština",
|
||||
};
|
||||
|
||||
export const LOCALES: Locale[] = Object.keys(LOCALE_NAMES) as Locale[];
|
||||
|
||||
// Locale storage key
|
||||
export const LOCALE_STORAGE_KEY = "infiplot:locale";
|
||||
|
||||
// Get locale from localStorage or browser language
|
||||
export function getInitialLocale(): Locale {
|
||||
if (typeof window === "undefined") return DEFAULT_LOCALE;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
if (stored && LOCALES.includes(stored as Locale)) {
|
||||
return stored as Locale;
|
||||
}
|
||||
} catch {
|
||||
// ignore localStorage errors
|
||||
}
|
||||
|
||||
// Try to match browser language
|
||||
const browserLang = navigator.language;
|
||||
const exactMatch = LOCALES.find((l) => l === browserLang);
|
||||
if (exactMatch) return exactMatch;
|
||||
|
||||
// Try base language match (e.g., "zh" for "zh-TW")
|
||||
const baseLang = browserLang.split("-")[0];
|
||||
if (baseLang) {
|
||||
const baseMatch = LOCALES.find((l) => l.startsWith(baseLang));
|
||||
if (baseMatch) return baseMatch;
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
// Save locale to localStorage
|
||||
export function setLocale(locale: Locale): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
||||
} catch {
|
||||
// ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
// Get RTL locales (right-to-left languages)
|
||||
export const RTL_LOCALES: Set<Locale> = new Set();
|
||||
|
||||
export function isRTL(locale: Locale): boolean {
|
||||
return RTL_LOCALES.has(locale);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Main i18n exports
|
||||
export * from "./config";
|
||||
export * from "./types";
|
||||
export * from "./utils";
|
||||
export { I18nProvider, useI18n, useTranslation } from "./client";
|
||||
export {
|
||||
getLocaleFromHeaders,
|
||||
loadTranslations,
|
||||
getTranslations,
|
||||
createTranslator,
|
||||
getServerLocale,
|
||||
} from "./server";
|
||||
|
||||
// Re-export locale types for convenience
|
||||
export type { Locale, LOCALES, LOCALE_NAMES } from "./config";
|
||||
@@ -0,0 +1,321 @@
|
||||
// Czech
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const cs = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot — AI interaktivní hra příběhů",
|
||||
"description": "InfiPlot je interaktivní hra příběhů, která používá AI k generování obsahu v reálném čase."
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [
|
||||
"Dětství přítelkyně se náhle zčervenal a přiznala mi lásku",
|
||||
"Po probuzení se zdá, že všechny dívky ve třídě mě tajně milují",
|
||||
"Uplynuly tři roky, ukázalo se, že jsem bohatý syn, čas na pomstu přišel",
|
||||
"Vrátil jsem se s nekonečným Tokenem těsně před vznikem internetu..."
|
||||
],
|
||||
"female": [
|
||||
"Přešla jsem do domu generála jako bezcenná dcera, ale chladný regent mě miluje jen mě",
|
||||
"Vrátila jsem se noc před rozchodem, tentokrát já jsem odešla první",
|
||||
"Probudila jsem se ve hře jako padouchova dcera, musím se vyhnout všem smrtícím koncům"
|
||||
],
|
||||
"x": [
|
||||
"Otevřela se trhlina v čase, různé verze mě z různých světů se náhle objevily",
|
||||
"V paláci paměti se zapomenuté fragmenty reformují do nového příběhu",
|
||||
"Začala nekonečná hra, každý má jedinou šanci na úspěch",
|
||||
"Systémové upozornění: Vaše volba rozhodne o osudu celého vesmíru"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"gender": "Zaměření pohlaví",
|
||||
"artStyle": "Umělecký styl",
|
||||
"plotStyle": "Styl příběhu",
|
||||
"voice": "Hlasové obsazení",
|
||||
"pacing": "Tempo obsahu"
|
||||
},
|
||||
"genders": {
|
||||
"male": "Mužské",
|
||||
"female": "Ženské",
|
||||
"x": "X"
|
||||
},
|
||||
"artStyles": {
|
||||
"auto": "Automatické",
|
||||
"custom": "Vlastní styl",
|
||||
"kyoani": "Kyoto Animation",
|
||||
"shinkai": "Makoto Shinkai",
|
||||
"ghibli": "Studio Ghibli",
|
||||
"3d": "3D animace",
|
||||
"cyberpunk": "Kyberpunk",
|
||||
"gothic": "Gotika",
|
||||
"wasteland": "Poustevna",
|
||||
"pixel": "Pixel art",
|
||||
"realistic": "Realistické",
|
||||
"oil": "Klasický olej",
|
||||
"monet": "Claude Monet",
|
||||
"watercolor": "Akvarel",
|
||||
"ink": "Inkoust",
|
||||
"ukiyoe": "Ukijo-e",
|
||||
"pencil": "Barevná tužka",
|
||||
"sketch": "Ruční skica",
|
||||
"manga": "Černobílá manga",
|
||||
"children": "Dětská kniha",
|
||||
"crayon": "Dětská kresba",
|
||||
"clay": "Hliněná plastika",
|
||||
"dunhuang": "Dunhuangské nástěnné malby",
|
||||
"miniature": "Miniatura",
|
||||
"mosaic": "Mozaika",
|
||||
"stainedGlass": "Skleněná mozaika",
|
||||
"vaporwave": "Vaporwave",
|
||||
"vector": "Vektorová ilustrace",
|
||||
"lowpoly": "Nízký počet polygonů",
|
||||
"popart": "Pop art",
|
||||
"glitch": "Glitch art",
|
||||
"papercut": "Paper cutting",
|
||||
"steampunk": "Steampunk",
|
||||
"xianxia": "Sien-šia",
|
||||
"darkFairytale": "Tmavá pohádka",
|
||||
"urbanFantasy": "Městská fantasy"
|
||||
},
|
||||
"plotStyles": {
|
||||
"straightforward": "Přímé a vzrušující",
|
||||
"twist": "Vícezávitkové"
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "Vypnuto",
|
||||
"on": "Zapnuto"
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "Rychlé a strhující",
|
||||
"relaxed": "Pomalejší a detailní"
|
||||
},
|
||||
"stories": {
|
||||
"贤者陨落": "Pád mudrce",
|
||||
"画中圣手": "Božská ruka v obraze",
|
||||
"花魁的刀": "Meč courtesan"
|
||||
},
|
||||
"ui": {
|
||||
"start": "Start",
|
||||
"loadStory": "Načíst příběh",
|
||||
"settings": "Nastavení",
|
||||
"searchPlaceholder": "Hledat styl...",
|
||||
"noMatchingStyle": "Žádný odpovídající styl",
|
||||
"close": "Zavřít",
|
||||
"back": "Zpět",
|
||||
"save": "Uložit",
|
||||
"cancel": "Zrušit",
|
||||
"saveAndSelect": "Uložit a vybrat"
|
||||
},
|
||||
"styleModal": {
|
||||
"title": "Vyberte umělecký styl",
|
||||
"subtitle": "Výchozí 'Automatické' · AI automaticky odpovídající styl podle příběhu; 'Vlastní styl' umožňuje zadat popis nebo nahrát referenční obrázek",
|
||||
"customTitle": "Vlastní styl",
|
||||
"customPlaceholder": "Popište požadovaný styl obrazu, například:\nSnnová akvarelová stylizace, jemné tóny, nostalgická atmosféra\n\n💡 Tip: Některé modely lépe pracují s anglickými popisy, doporučuje použít AI nástroj k vytvoření profesionálního anglického popisu",
|
||||
"uploadImage": "Nahrát referenční obrázek",
|
||||
"changeImage": "Změnit",
|
||||
"remove": "Odebrat",
|
||||
"parsing": "Zpracování...",
|
||||
"importFromPreset": "Importovat z přednastaveného stylu...",
|
||||
"uploadError": "Podporovány jsou pouze obrazové soubory",
|
||||
"visionError": "Vision model vrátil prázdný popis stylu",
|
||||
"fileReadError": "Čtení souboru selhalo",
|
||||
"imageDecodeError": "Dekódování obrazu selhalo",
|
||||
"parseError": "Zpracování selhalo",
|
||||
"refImageAlt": "Referenční obrázek stylu"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Jaký příběh chcete dnes zažít?",
|
||||
"placeholder": "Omlouváme se, ale nemohu splnit tento požadavek.",
|
||||
"enterHint": "Enter k odeslání · Shift+Enter nový řádek"
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (během bety vyžadováno přihlášení, hra zdarma)' : '';
|
||||
return `Zadejte své nápady, nakonfigurujte styly a klikněte na "Spustit" pro hraní${authHint}. Můžete si také vybrat kurátorskou příběh z níže pro rychlé zážitky <em>InfiPlot</em>. Klikněte na "Nastavení" pro zadání vašeho jména a konfiguraci vlastních klíčů pro text, obrázky, vizi a TTS—vše uloženo lokálně ve vašem prohlížeči pro stabilnější zážitek.`;
|
||||
},
|
||||
"closeAriaLabel": "Znovu nezobrazovat tuto nápovědu"
|
||||
},
|
||||
"about": {
|
||||
"title": "InfiPlot",
|
||||
"description": "je interaktivní hra příběhů, která používá AI k generování obsahu v reálném čase — obrázky, zvuk a větve příběhu jsou generovány během hraní.",
|
||||
"team": "TÝM",
|
||||
"teamText": "Pocházíme z univerzit včetně Tsinghua University a Lanzhou University, chceme prozkoumat více možností multimodálních modelů mimo schopnosti jako 'přímé generování obrázků a videí'. Tento projekt je stále v rané fázi, stále rekrutujeme členy. Pokud máte zájem, kontaktujte nás, těšíme se na vás.",
|
||||
"contact": "KONTAKT",
|
||||
"email": "E-mail",
|
||||
"openSource": "OTEVŘENÝ ZDROJ",
|
||||
"betaUsers": "BETA TESTERI",
|
||||
"qqGroupLabel": "Skupina QQ:",
|
||||
"qqGroupAlt": "InfiPlot veřejná beta skupina QR kód (ID skupiny 575404333)",
|
||||
"privacyPolicy": "Zásady ochrany soukromí",
|
||||
"terms": "Podmínky služby",
|
||||
"copyright": "© 2026 InfiPlot. Všechna práva vyhrazena."
|
||||
},
|
||||
"errors": {
|
||||
"emptyFile": "Tento soubor příběhu je prázdný.",
|
||||
"fileTooLarge": "Soubor příběhu je příliš velký.",
|
||||
"unpackFailed": "Rozbalení souboru příběhu selhalo.",
|
||||
"parseFailed": "Zpracování souboru příběhu selhalo.",
|
||||
"cardNotFound": "Curated story nebyl nalezen: {cardName}"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
"loading": {
|
||||
"firstFrame": "Načítání prvního scény",
|
||||
"transitioning": "AI vytváří další scénu",
|
||||
"visionThinking": "AI přemýšlí co jste viděli",
|
||||
"loadingFirst": "První scény se načítá",
|
||||
"awakening": "Načítání"
|
||||
},
|
||||
"freeform": {
|
||||
"placeholder": "Zadejte co chcete říct nebo udělat...",
|
||||
"title": "Volný vstup",
|
||||
"ariaLabel": "Volný vstup"
|
||||
},
|
||||
"choiceDisabled": "Sdílený příběh neobsahuje tuto větev",
|
||||
"tooltips": {
|
||||
"openSettings": "Otevřít nastavení",
|
||||
"openHistory": "Historie příběhu",
|
||||
"fullscreen": "Režim celé obrazovky (F)",
|
||||
"enterFullscreen": "Vstoupit do režimu celé obrazovky",
|
||||
"exportGallery": "Exportovat jako interaktivní galerii",
|
||||
"exportGalleryLabel": "Exportovat galerii",
|
||||
"shareStory": "Exportovat příběh jako .infiplot",
|
||||
"shareStoryLabel": "Sdílet aktuální příběh",
|
||||
"mute": "Ztlumit",
|
||||
"unmute": "Zrušit ztlumení",
|
||||
"closeNudge": "Zavřít nápovědu",
|
||||
"silenceNudge": "Nejste spokojeni? Zkuste zadat vlastní API klíč",
|
||||
"back": "Zpět"
|
||||
},
|
||||
"imageAlt": "Vygenerovaná scéna",
|
||||
"counter": {
|
||||
"scene": "Scéna {n}",
|
||||
"beat": "Beat {n}",
|
||||
"middle": "·"
|
||||
},
|
||||
"buttons": {
|
||||
"fullscreen": "F · klávesa · celá obrazovka",
|
||||
"exportGallery": "Exportovat galerii",
|
||||
"shareStory": "Sdílet příběh",
|
||||
"muted": "Ztlumeno",
|
||||
"sound": "Se zvukem"
|
||||
},
|
||||
"error": {
|
||||
"title": "Došlo k chybě",
|
||||
"back": "Zpět"
|
||||
},
|
||||
"previousStep": "Předchozí krok",
|
||||
"settingsFooter": "Po uložení se TTS klíč okamžitě uplatní, použijte svůj limit k syntéze zvuku pro aktuální scénu.",
|
||||
"shareErrors": {
|
||||
"notFound": "Soubor příběhu nebyl nalezen.",
|
||||
"invalid": "Sdílený soubor příběhu neobsahuje platný příběh.",
|
||||
"noImage": "Sdílený soubor postrádá první obrázek scény.",
|
||||
"noNextImage": "Sdílený soubor postrádá další obrázek scény.",
|
||||
"noMemory": "Sdílený soubor postrádá počáteční paměť příběhu.",
|
||||
"packFailed": "Balení sdíleného příběhu selhalo"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Nastavení",
|
||||
"subtitle": "Volitelné · Tato nastavení jsou uložena pouze v místním prohlížeči",
|
||||
"tabs": {
|
||||
"general": "Obecné",
|
||||
"models": "Modely"
|
||||
},
|
||||
"general": {
|
||||
"playerName": "Jméno hráče",
|
||||
"playerNamePlaceholder": "Prázdné použije 'vy'",
|
||||
"playerNameHint": "NPC budou oslovoováni tímto jménem v dialogu.",
|
||||
"visionClick": "Kliknutí pro identifikaci scény",
|
||||
"visionOn": "Zapnuto",
|
||||
"visionOff": "Vypnuto",
|
||||
"visionHint": "Po zapnutí kliknutí na scénu ve výběrovém uzlu spustí AI identifikaci a vygeneruje novou větev příběhu."
|
||||
},
|
||||
"models": {
|
||||
"corsNotice": "Ujistěte se, že váš API endpoint podporuje CORS požadavky z prohlížeče. Většina hlavních poskytovatelů (OpenAI, Anthropic, Gemini, Runware atd.) již ve výchozím nastavení podporuje.",
|
||||
"textModel": "Textový model",
|
||||
"imageModel": "Obrazový model",
|
||||
"visionModel": "Vision model",
|
||||
"baseUrl": "Základní URL",
|
||||
"apiKey": "API klíč",
|
||||
"model": "Model",
|
||||
"provider": "Poskytovatel (volitelné)",
|
||||
"providerHint": "Při prázdném systém automaticky odvodí protokol podle základní URL.",
|
||||
"providerAuto": "Automatické odvození (doporučeno)",
|
||||
"show": "Zobrazit",
|
||||
"hide": "Skrýt"
|
||||
},
|
||||
"tts": {
|
||||
"title": "Model hlasového doprovodu",
|
||||
"description": 'Zadejte svůj <span class="text-clay-800">Xiaomi MiMo API klíč</span>, hlasový doprovod bude syntetizován lokálně v prohlížeči, klíč je uložen pouze lokálně. MiMo TTS je nyní<span class="text-clay-800">zdarma</span>.',
|
||||
"keyType": "Typ klíče",
|
||||
"payg": "Pay-as-you-go",
|
||||
"paygSub": "Začíná na sk-",
|
||||
"tokenPlan": "Token plán",
|
||||
"tokenPlanSub": "Začíná na tp-",
|
||||
"region": "Regionální uzel",
|
||||
"regionHint": "Vyberte uzel odpovídající vaší oblasti předplatného.",
|
||||
"apiKeyPlaceholderPayg": "Vložte klíč začínající na sk-",
|
||||
"apiKeyPlaceholderToken": "Vložte klíč začínající na tp-",
|
||||
"keyMismatchPayg": "Tento klíč nezačíná na sk-",
|
||||
"keyMismatchToken": "Tento klíč nezačíná na tp-",
|
||||
"tutorialLink": "Jak získat klíč zdarma? Zobrazit tutoriál"
|
||||
},
|
||||
"actions": {
|
||||
"save": "Uložit",
|
||||
"clearAll": "Vymazat vše"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"steps": {
|
||||
"pick": "Přihlaste se pro pokračování",
|
||||
"email": "Přihlášení e-mailem",
|
||||
"otp": "Ověřovací kód"
|
||||
},
|
||||
"googleLogin": "Přihlášení Google",
|
||||
"githubLogin": "Přihlášení GitHub",
|
||||
"emailLogin": "Přihlášení ověřovacím kódem e-mailem",
|
||||
"or": "Nebo",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"sendCode": "Odeslat kód",
|
||||
"sending": "Odesílání...",
|
||||
"codeSent": "Ověřovací kód byl odeslán na {email}",
|
||||
"codePlaceholder": "6místný ověřovací kód",
|
||||
"verify": "Potvrdit",
|
||||
"verifying": "Ověřování...",
|
||||
"resend": "Znovu odeslat",
|
||||
"back": "Zpět",
|
||||
"close": "Zavřít",
|
||||
"ariaLabel": "Přihlášení"
|
||||
},
|
||||
"history": {
|
||||
"title": "Historie příběhu",
|
||||
"close": "Zavřít",
|
||||
"closeAriaLabel": "Zavřít historii příběhu",
|
||||
"noHistory": "Zatím žádná historie.",
|
||||
"scene": "Scéna {n}",
|
||||
"choice": "Volba",
|
||||
"action": "Akce",
|
||||
"ariaLabel": "Historie příběhu"
|
||||
},
|
||||
"customForm": {
|
||||
"world": "Svět · Světový názor",
|
||||
"style": "Styl · Vizuální styl",
|
||||
"worldPlaceholder": "Příklad: Jihočínský okresální město koncem 90. let. Hlavní postava je přestupující student v posledním ročníku střední školy, který v deštivém červnu potkává spolužáka, který čte básně na střeše.",
|
||||
"stylePlaceholder": "Příklad: Akvarelové měkké světlo, teplé odpolední světlo, styl vizuální novely, tradiční panel dialogu...",
|
||||
"status": {
|
||||
"ready": "Připraven",
|
||||
"needMore": "Je třeba ještě dva odstavce",
|
||||
"starting": "První scény se načítá..."
|
||||
},
|
||||
"start": "Spustit"
|
||||
},
|
||||
"language": {
|
||||
"title": "Jazyk",
|
||||
"current": "Aktuální jazyk",
|
||||
"select": "Vybrat jazyk"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type csTranslations = typeof cs;
|
||||
@@ -0,0 +1,89 @@
|
||||
// German (Germany)
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const de = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (Anmeldung während der Beta erforderlich, kostenloses Spielen)' : '';
|
||||
return `Gib deine Ideen ein, konfiguriere Stile und klicke auf "Starten" zum Spielen${authHint}. Du kannst auch eine kuratierte Geschichte unten auswählen, um <em>InfiPlot</em> schnell zu erleben. Klicke auf "Einstellungen", um deinen Namen einzugeben und deine eigenen Text-, Bild-, Vision- und TTS-Schlüssel zu konfigurieren—alles wird lokal in deinem Browser für eine stabilere Erfahrung gespeichert.`;
|
||||
},
|
||||
"closeAriaLabel": "Diesen Hinweis nicht mehr anzeigen"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type DeTranslations = typeof de;
|
||||
@@ -0,0 +1,387 @@
|
||||
// English (en) - Base English translations
|
||||
// This is a manually translated reference file
|
||||
|
||||
export const en = {
|
||||
// ========== Layout ==========
|
||||
layout: {
|
||||
metadata: {
|
||||
title: "InfiPlot — AI Real-time Interactive Story Game",
|
||||
description: "InfiPlot is an interactive story game demo that uses AI to generate images, voice, and story branches in real-time.",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Home Page ==========
|
||||
home: {
|
||||
examples: {
|
||||
male: [
|
||||
"My childhood friend suddenly blushes and confesses her feelings to me",
|
||||
"I wake up one day and find that all the girls in my class seem to have secretly fallen in love with me",
|
||||
"The three-year deadline has arrived. Turns out I'm a wealthy heir, and the time for revenge is now",
|
||||
"I travel back to the eve of the internet's birth with unlimited tokens...",
|
||||
],
|
||||
female: [
|
||||
"Transmigrated as the useless daughter of a general's mansion, the cold regent only dotes on me",
|
||||
"Reborn on the night before our breakup, this time I'll be the one to let go first",
|
||||
"I wake up as a villainess in an otome game and must avoid all death endings",
|
||||
],
|
||||
x: [
|
||||
"The spacetime rift opens, and versions of myself from parallel worlds suddenly appear",
|
||||
"In the memory palace, forgotten fragments are reassembling into a new story",
|
||||
"An infinite flow game begins—everyone has only one chance to clear it",
|
||||
"System notification: your choice will determine the fate of the entire universe",
|
||||
],
|
||||
},
|
||||
|
||||
options: {
|
||||
gender: "Orientation",
|
||||
artStyle: "Art Style",
|
||||
plotStyle: "Plot Style",
|
||||
voice: "Voice",
|
||||
pacing: "Pacing",
|
||||
},
|
||||
|
||||
genders: {
|
||||
male: "Male-oriented",
|
||||
female: "Female-oriented",
|
||||
x: "Universal",
|
||||
},
|
||||
|
||||
artStyles: {
|
||||
auto: "Auto",
|
||||
custom: "Custom Style",
|
||||
kyoani: "Kyoto Animation",
|
||||
shinkai: "Makoto Shinkai",
|
||||
ghibli: "Studio Ghibli",
|
||||
"3d": "3D Animation",
|
||||
cyberpunk: "Cyberpunk",
|
||||
gothic: "Gothic",
|
||||
wasteland: "Wasteland",
|
||||
pixel: "Pixel Art",
|
||||
realistic: "Realistic",
|
||||
oil: "Classical Oil",
|
||||
monet: "Monet",
|
||||
watercolor: "Watercolor",
|
||||
ink: "Ink Wash",
|
||||
ukiyoe: "Ukiyo-e",
|
||||
pencil: "Colored Pencil",
|
||||
sketch: "Hand-drawn Sketch",
|
||||
manga: "Black & White Manga",
|
||||
children: "Children's Picture Book",
|
||||
crayon: "Crayon Drawing",
|
||||
clay: "Clay Art",
|
||||
dunhuang: "Dunhuang Mural",
|
||||
miniature: "Miniature",
|
||||
mosaic: "Mosaic",
|
||||
stainedGlass: "Stained Glass",
|
||||
vaporwave: "Vaporwave",
|
||||
vector: "Vector Art",
|
||||
lowpoly: "Low Poly",
|
||||
popart: "Pop Art",
|
||||
glitch: "Glitch Art",
|
||||
papercut: "Papercut Art",
|
||||
steampunk: "Steampunk",
|
||||
xianxia: "Xianxia Fantasy",
|
||||
darkFairytale: "Dark Fairytale",
|
||||
urbanFantasy: "Urban Fantasy",
|
||||
},
|
||||
|
||||
plotStyles: {
|
||||
straightforward: "Linear",
|
||||
twist: "Multi-branch",
|
||||
suspense: "Suspenseful",
|
||||
healing: "Slice-of-life",
|
||||
},
|
||||
|
||||
voiceOptions: {
|
||||
off: "Off",
|
||||
on: "On",
|
||||
},
|
||||
|
||||
pacings: {
|
||||
slow: "Slow-burn",
|
||||
fast: "Brisk",
|
||||
},
|
||||
|
||||
stories: {
|
||||
sage_downfall: "Sage's Downfall",
|
||||
brush_sage: "Painter Sage",
|
||||
courtesan_blade: "Courtesan's Blade",
|
||||
},
|
||||
|
||||
ui: {
|
||||
start: "Start",
|
||||
loadStory: "Load Story",
|
||||
settings: "Settings",
|
||||
searchPlaceholder: "Search styles…",
|
||||
noMatchingStyle: "No matching styles",
|
||||
close: "Close",
|
||||
back: "Back",
|
||||
save: "Save",
|
||||
cancel: "Cancel",
|
||||
saveAndSelect: "Save and Select",
|
||||
},
|
||||
|
||||
styleModal: {
|
||||
title: "Select Art Style",
|
||||
subtitle: 'Default "Auto" · AI automatically matches the style to your story; select "Custom Style" to enter a description or upload a reference image',
|
||||
customTitle: "Custom Style",
|
||||
customPlaceholder: `Describe the visual style you want, for example:
|
||||
Dreamy watercolor style with soft tones and nostalgic atmosphere
|
||||
|
||||
💡 Tip: Some image models work better with English prompts. Consider using an AI chatbot to generate professional English style descriptions first, then paste them here.`,
|
||||
uploadImage: "Upload Reference",
|
||||
changeImage: "Change Image",
|
||||
remove: "Remove",
|
||||
parsing: "Parsing…",
|
||||
importFromPreset: "Import from Preset…",
|
||||
uploadError: "Only image files are supported",
|
||||
visionError: "Vision model returned an empty style description",
|
||||
fileReadError: "Failed to read file",
|
||||
imageDecodeError: "Failed to decode image",
|
||||
parseError: "Failed to parse",
|
||||
refImageAlt: "Style reference image",
|
||||
},
|
||||
|
||||
hero: {
|
||||
title: "What story do you want to experience today?",
|
||||
placeholder: " ",
|
||||
enterHint: "Enter to send · Shift+Enter for newline",
|
||||
},
|
||||
|
||||
hint: {
|
||||
text: (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (login required during beta, free to play)' : '';
|
||||
return `Enter your ideas, configure styles, and click "Start" to play${authHint}. You can also pick a curated story from below to quickly experience <em class="not-italic text-ember-500">InfiPlot</em>. Click "<span class="inline-flex items-center gap-1 text-ember-500"><i class="fa-solid fa-gear text-[10px]"></i>Settings</span>" to enter your name and configure your own text, image, vision models and TTS keys—all stored locally in your browser for a more stable experience.`;
|
||||
},
|
||||
closeAriaLabel: "Don't show this hint again",
|
||||
},
|
||||
|
||||
about: {
|
||||
title: "InfiPlot",
|
||||
description: "is an interactive story game that uses AI to generate content in real-time — images, voice, and story branches are all generated during gameplay.",
|
||||
team: "TEAM",
|
||||
teamText: "We are from universities including Tsinghua University and Lanzhou University, hoping to explore more possibilities of multimodal models beyond oneshot capabilities like direct image and video generation. This project is still in its early stages, and we are recruiting. If you're interested, please contact us—we look forward to your joining.",
|
||||
contact: "CONTACT",
|
||||
email: "Email",
|
||||
openSource: "OPEN SOURCE",
|
||||
betaUsers: "BETA USERS",
|
||||
qqGroupLabel: "QQ Group: ",
|
||||
qqGroupAlt: "InfiPlot Public Beta Group QR Code (Group ID: 575404333)",
|
||||
legalNotice: (params: { analyticsOn?: boolean }) => {
|
||||
const base = "During public beta, this product is free to use but stability may vary with concurrent user load.<br />Content generated during public beta is not saved on servers. To preserve your experience, use the export gallery or story sharing features after playing.<br />AI-generated content does not represent our team's stance.";
|
||||
if (params.analyticsOn) {
|
||||
return `${base}<br />This site uses open-source <a href="https://umami.is/" target="_blank" rel="noopener noreferrer">Umami</a> for privacy-friendly anonymous analytics: no cookies, no personal data collection, no transmission of your inputs, no cross-site tracking.`;
|
||||
}
|
||||
return base;
|
||||
},
|
||||
privacyPolicy: "Privacy Policy",
|
||||
terms: "Terms of Service",
|
||||
copyright: "© 2026 InfiPlot. All rights reserved.",
|
||||
},
|
||||
|
||||
errors: {
|
||||
emptyFile: "This story file is empty.",
|
||||
fileTooLarge: "The story file is too large to load.",
|
||||
unpackFailed: "Failed to unpack the story file.",
|
||||
parseFailed: "Failed to parse the story file.",
|
||||
cardNotFound: "Curated story not found: {cardName}",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Play Page ==========
|
||||
// NOTE: zh-CN uses " · " between every character as a stylistic effect.
|
||||
// Other locales MUST NOT use this dot separator — just plain words.
|
||||
play: {
|
||||
loading: {
|
||||
firstFrame: "Drawing the first scene",
|
||||
transitioning: "AI is painting the next scene",
|
||||
visionThinking: "AI is interpreting what you see",
|
||||
loadingFirst: "Awakening the first scene",
|
||||
awakening: "Loading",
|
||||
},
|
||||
|
||||
freeform: {
|
||||
placeholder: "Enter what you want to say or do...",
|
||||
title: "Free Input",
|
||||
ariaLabel: "Free input",
|
||||
},
|
||||
|
||||
choiceDisabled: "This branch is not included in the shared story",
|
||||
|
||||
tooltips: {
|
||||
openSettings: "Open Settings",
|
||||
openHistory: "Story History",
|
||||
fullscreen: "Fullscreen (F)",
|
||||
enterFullscreen: "Enter Fullscreen",
|
||||
exportGallery: "Export current session as interactive gallery link (with voice; keeps only the 2 most recent gallery links)",
|
||||
exportGalleryLabel: "Export Interactive Gallery",
|
||||
shareStory: "Export current session as playable .infiplot story file (with voice)",
|
||||
shareStoryLabel: "Share Current Story",
|
||||
mute: "Mute",
|
||||
unmute: "Unmute",
|
||||
closeNudge: "Close hint",
|
||||
silenceNudge: "Poor quality/often silent? Try entering your own API Key",
|
||||
back: "Back",
|
||||
},
|
||||
|
||||
imageAlt: "Generated scene",
|
||||
|
||||
counter: {
|
||||
scene: "Scene {n}",
|
||||
beat: "Frame {n}",
|
||||
middle: " ",
|
||||
},
|
||||
|
||||
buttons: {
|
||||
fullscreen: "Fullscreen",
|
||||
exportGallery: "Export Gallery",
|
||||
shareStory: "Share Story",
|
||||
muted: "Muted",
|
||||
sound: "Sound",
|
||||
},
|
||||
|
||||
error: {
|
||||
title: "Something went wrong",
|
||||
back: "Back",
|
||||
},
|
||||
|
||||
previousStep: "Previous action",
|
||||
|
||||
settingsFooter: "After saving, the voice key takes effect immediately and uses your quota to synthesize voice for the current scene.",
|
||||
|
||||
shareErrors: {
|
||||
notFound: "No story file found to load.",
|
||||
invalid: "Story share file has no playable content.",
|
||||
noImage: "Story share file is missing the first scene image.",
|
||||
noNextImage: "Story share file is missing the next scene image.",
|
||||
noMemory: "Story share file is missing initial story memory and cannot be loaded.",
|
||||
packFailed: "Failed to pack story share",
|
||||
},
|
||||
|
||||
exportProgress: {
|
||||
preparingVoice: "Preparing voice",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Settings Modal ==========
|
||||
settings: {
|
||||
title: "Settings",
|
||||
subtitle: "Optional · These settings are saved only in your local browser",
|
||||
|
||||
tabs: {
|
||||
general: "General",
|
||||
models: "Models",
|
||||
},
|
||||
|
||||
general: {
|
||||
playerName: "Player Name",
|
||||
playerNamePlaceholder: "Leave empty to use 'You'",
|
||||
playerNameHint: "NPCs will address you by this name in dialogue. If left empty, 'You' will be used by default.",
|
||||
visionClick: "Click Image Recognition",
|
||||
visionOn: "On",
|
||||
visionOff: "Off",
|
||||
visionHint: "When enabled, clicking on the image at choice nodes will trigger AI vision recognition and generate new story branches.",
|
||||
},
|
||||
|
||||
models: {
|
||||
corsNotice: "Please ensure your API endpoint supports browser CORS requests. Most mainstream providers (OpenAI, Anthropic, Gemini, Runware, etc.) support this by default.",
|
||||
textModel: "Text Model",
|
||||
imageModel: "Image Model",
|
||||
visionModel: "Vision Model",
|
||||
baseUrl: "BASE URL",
|
||||
apiKey: "API Key",
|
||||
model: "Model",
|
||||
provider: "Provider (Optional)",
|
||||
providerHint: "Leave empty for the system to auto-detect the protocol based on the Base URL.",
|
||||
providerAuto: "Auto-detect (Recommended)",
|
||||
show: "Show",
|
||||
hide: "Hide",
|
||||
},
|
||||
|
||||
tts: {
|
||||
title: "Voice Model",
|
||||
description: 'Enter your own <span class="text-clay-800">Xiaomi MiMo API Key</span>. Voice synthesis runs locally in your browser, and the key is saved locally and never sent to the server. MiMo TTS is currently <span class="text-clay-800">free for a limited time</span>—just apply to use it.',
|
||||
keyType: "Key Type",
|
||||
payg: "Pay-as-you-go",
|
||||
paygSub: "Starts with sk-",
|
||||
tokenPlan: "Token Plan",
|
||||
tokenPlanSub: "Starts with tp-",
|
||||
region: "Region Node",
|
||||
regionHint: "Select the node matching your subscription region (usually the one with lowest latency).",
|
||||
apiKeyPlaceholderPayg: "Paste sk- pay-as-you-go key",
|
||||
apiKeyPlaceholderToken: "Paste tp- token plan key",
|
||||
keyMismatchPayg: 'This key does not start with sk-. It may not match the selected "Pay-as-you-go" type. Please check if you entered it correctly.',
|
||||
keyMismatchToken: 'This key does not start with tp-. It may not match the selected "Token Plan" type. Please check if you entered it correctly.',
|
||||
tutorialLink: "How to get a free key? View tutorial",
|
||||
},
|
||||
|
||||
actions: {
|
||||
save: "Save",
|
||||
clearAll: "Clear All",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Auth Modal ==========
|
||||
auth: {
|
||||
steps: {
|
||||
pick: "Login to Continue",
|
||||
email: "Email Login",
|
||||
otp: "Verification Code",
|
||||
},
|
||||
|
||||
googleLogin: "Continue with Google",
|
||||
githubLogin: "Continue with GitHub",
|
||||
emailLogin: "Email Verification Code",
|
||||
or: "or",
|
||||
|
||||
emailPlaceholder: "your@email.com",
|
||||
sendCode: "Send Code",
|
||||
sending: "Sending...",
|
||||
|
||||
codeSent: "Verification code sent to {email}",
|
||||
codePlaceholder: "6-digit code",
|
||||
verify: "Confirm",
|
||||
verifying: "Verifying...",
|
||||
resend: "Resend",
|
||||
|
||||
back: "Back",
|
||||
|
||||
close: "Close",
|
||||
ariaLabel: "Login",
|
||||
},
|
||||
|
||||
// ========== Dialogue History Modal ==========
|
||||
history: {
|
||||
title: "Story History",
|
||||
close: "Close",
|
||||
closeAriaLabel: "Close story history",
|
||||
noHistory: "No history yet.",
|
||||
scene: "Scene {n}",
|
||||
choice: "Choice",
|
||||
action: "Action",
|
||||
ariaLabel: "Story history",
|
||||
},
|
||||
|
||||
// ========== Custom Form ==========
|
||||
customForm: {
|
||||
world: "World",
|
||||
style: "Style",
|
||||
worldPlaceholder: "Example: A small county town in southern China in the late 1990s. The protagonist is a transfer student in senior year who meets a classmate always reading poetry on the rooftop during the rainy June. Slow-burn, subtle, slightly melancholic...",
|
||||
stylePlaceholder: "Example: Watercolor soft light, afternoon warmth, anime visual novel style, traditional dialogue panel...",
|
||||
status: {
|
||||
ready: "Ready",
|
||||
needMore: "Two more to go",
|
||||
starting: "Waking first frame…",
|
||||
},
|
||||
start: "Start",
|
||||
},
|
||||
|
||||
// ========== Language Switcher ==========
|
||||
language: {
|
||||
title: "Language",
|
||||
current: "Current Language",
|
||||
select: "Select Language",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type EnTranslations = typeof en;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Spanish
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const es = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (se requiere inicio de sesión durante la beta, juego gratuito)' : '';
|
||||
return `Ingresa tus ideas, configura estilos y haz clic en "Iniciar" para jugar${authHint}. También puedes elegir una historia curada de abajo para experimentar rápidamente <em>InfiPlot</em>. Haz clic en "Configuración" para ingresar tu nombre y configurar tus propias claves de texto, imagen, visión y TTS—todo almacenado localmente en tu navegador para una experiencia más estable.`;
|
||||
},
|
||||
"closeAriaLabel": "No volver a mostrar este consejo"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type EsTranslations = typeof es;
|
||||
@@ -0,0 +1,89 @@
|
||||
// French
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const fr = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (connexion requise pendant la bêta, jeu gratuit)' : '';
|
||||
return `Entrez vos idées, configurez les styles et cliquez sur "Démarrer" pour jouer${authHint}. Vous pouvez également choisir une histoire sélectionnée ci-dessous pour découvrir rapidement <em>InfiPlot</em>. Cliquez sur "Paramètres" pour entrer votre nom et configurer vos propres clés de texte, d'image, de vision et de TTS—tout est stocké localement dans votre navigateur pour une expérience plus stable.`;
|
||||
},
|
||||
"closeAriaLabel": "Ne plus afficher cette astuce"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type FrTranslations = typeof fr;
|
||||
@@ -0,0 +1,321 @@
|
||||
// Hindi
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const hi = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot — AI रीयल-टाइम इंटरैक्टिव स्टोरी गेम",
|
||||
"description": "InfiPlot एक इंटरैक्टिव स्टोरी गेम है जो AI का उपयोग करके रीयल-टाइम में सामग्री उत्पन्न करता है।"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [
|
||||
"बचपन की सहेली ने अचानक शर्माते हुए मुझसे प्यार का इज़हार किया",
|
||||
"एक नींद के बाद जागने पर लगा कि कक्षा की सभी लड़कियां चुपके से मुझसे प्यार करने लगी हैं",
|
||||
"तीन साल की अवधि समाप्त, अब पता चला मैं एक अमीर परिवार का बेटा हूं, बदला लेने का समय आ गया है",
|
||||
"मैं अनंत टोकन लेकर इंटरनेट के जन्म से ठीक पहले वापस आ गया हूं..."
|
||||
],
|
||||
"female": [
|
||||
"जनरल के घर की बेकार बेटी में बदल गई, लेकिन ठंडे राजकुमार ने केवल मुझे चाहा",
|
||||
"संबंध-विच्छेद से एक रात पहले वापस आ गई, इस बार मैंने पहले हाथ उठाए",
|
||||
"एक खेल में खलनायिका की बेटी बन गई, सभी मृत्यु अंत से बचना है"
|
||||
],
|
||||
"x": [
|
||||
"समय-स्थान विदर में खुल गया, कई समानांतर दुनिया के स्वयं अचानक सामने आ गए",
|
||||
"स्मृति महल में, वे भूले हुए टुकड़े नई कहानी में पुनर्गठित हो रहे हैं",
|
||||
"एक अनंत खेल शुरू हो गया, सभी के पास एक अनूठा मौका है",
|
||||
"सिस्टम संकेत: आपकी पसंद पूरे ब्रह्मांड के भाग्य को निर्धारित करेगी"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"gender": "लिंग झुकाव",
|
||||
"artStyle": "कला शैली",
|
||||
"plotStyle": "कथा शैली",
|
||||
"voice": "आवाज डबिंग",
|
||||
"pacing": "गति"
|
||||
},
|
||||
"genders": {
|
||||
"male": "पुरुष-ओरिएंटेड",
|
||||
"female": "महिला-ओरिएंटेड",
|
||||
"x": "X"
|
||||
},
|
||||
"artStyles": {
|
||||
"auto": "स्वचालित",
|
||||
"custom": "कस्टम शैली",
|
||||
"kyoani": "क्योटो एनीमेशन",
|
||||
"shinkai": "माकोतो शिंकाई",
|
||||
"ghibli": "घिबली स्टूडियो",
|
||||
"3d": "3D एनीमेशन",
|
||||
"cyberpunk": "साइबरपंक",
|
||||
"gothic": "गॉथिक",
|
||||
"wasteland": "बंजर भूमि",
|
||||
"pixel": "पिक्सेल आर्ट",
|
||||
"realistic": "यथार्थवादी",
|
||||
"oil": "शास्त्रीय तेल चित्र",
|
||||
"monet": "क्लाउद मोने",
|
||||
"watercolor": "जल रंग",
|
||||
"ink": "स्याही चित्र",
|
||||
"ukiyoe": "उकियो-ए",
|
||||
"pencil": "रंगीन पेंसिल",
|
||||
"sketch": "हाथ से बनाया गया स्केच",
|
||||
"manga": "श्वेत-श्याम मंगा",
|
||||
"children": "बाल साहित्य",
|
||||
"crayon": "बच्चों की क्रेयन चित्र",
|
||||
"clay": "मिट्टी की कला",
|
||||
"dunhuang": "दुनहुआंग दीवार चित्र",
|
||||
"miniature": "लघु चित्र",
|
||||
"mosaic": "मोज़ेक",
|
||||
"stainedGlass": "दाग़ीन कांच",
|
||||
"vaporwave": "वेपरवेव",
|
||||
"vector": "वेक्टर चित्र",
|
||||
"lowpoly": "कम पोलीगॉन",
|
||||
"popart": "पॉप आर्ट",
|
||||
"glitch": "ग्लिच आर्ट",
|
||||
"papercut": "कागज़ काटना कला",
|
||||
"steampunk": "स्टीमपंक",
|
||||
"xianxia": "सियानशिया",
|
||||
"darkFairytale": "अंधेरी परी कथा",
|
||||
"urbanFantasy": "शहरी कल्पना"
|
||||
},
|
||||
"plotStyles": {
|
||||
"straightforward": "सीधी रोमांचक",
|
||||
"twist": "बहु-मोड़ी रोमांचक"
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "बंद",
|
||||
"on": "चालू"
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "तेज़ और रोमांचक",
|
||||
"relaxed": "धीरे और विस्तृत"
|
||||
},
|
||||
"stories": {
|
||||
"贤者陨落": "ऋषि का पतन",
|
||||
"画中圣手": "चित्र में दिव्य हाथ",
|
||||
"花魁的刀": "वेश्या की तलवार"
|
||||
},
|
||||
"ui": {
|
||||
"start": "शुरू",
|
||||
"loadStory": "कहानी लोड करें",
|
||||
"settings": "सेटिंग्स",
|
||||
"searchPlaceholder": "शैली खोजें...",
|
||||
"noMatchingStyle": "कोई मेल खाने वाली शैली नहीं",
|
||||
"close": "बंद करें",
|
||||
"back": "वापस",
|
||||
"save": "सहेजें",
|
||||
"cancel": "रद्द करें",
|
||||
"saveAndSelect": "सहेजें और चुनें"
|
||||
},
|
||||
"styleModal": {
|
||||
"title": "कला शैली चुनें",
|
||||
"subtitle": "डिफ़ॉल्ट 'स्वचालित' · AI कहानी के अनुसार शैली स्वचालित रूप से मिलाता है; 'कस्टम शैली' चुनकर आप विवरण दे सकते हैं या संदर्भ चित्र अपलोड कर सकते हैं",
|
||||
"customTitle": "कस्टम शैली",
|
||||
"customPlaceholder": "अपनी इच्छित शैली का वर्णन करें, उदाहरण के लिए:\nस्वप्निल जल रंग शैली, कोमल रंग, पुरानी यादें\n\n💡 संकेत: कुछ ड्रॉइंग मॉडल के लिए अंग्रेजी संकेत शब्द बेहतर काम करते हैं, एआई वार्ता टूल का उपयोग करके पेशेवर अंग्रेजी शैली विवरण उत्पन्न करने का सुझाव दिया जाता है",
|
||||
"uploadImage": "संदर्भ चित्र अपलोड करें",
|
||||
"changeImage": "बदलें",
|
||||
"remove": "हटाएं",
|
||||
"parsing": "विश्लेषण हो रहा है...",
|
||||
"importFromPreset": "प्रीसेट शैली से आयात करें...",
|
||||
"uploadError": "केवल चित्र फ़ाइल समर्थित है",
|
||||
"visionError": "दृश्य मॉडल ने खाली शैली विवरण लौटाया",
|
||||
"fileReadError": "फ़ाइल पढ़ने में विफल",
|
||||
"imageDecodeError": "चित्र को डिकोड करने में विफल",
|
||||
"parseError": "विश्लेषण में विफल",
|
||||
"refImageAlt": "शैली संदर्भ चित्र"
|
||||
},
|
||||
"hero": {
|
||||
"title": "आज कौन सी कहानी का अनुभव करना चाहते हैं?",
|
||||
"placeholder": "माफ़ कीजिए, मैं उस अनुरोध को पूरा नहीं कर सकता।",
|
||||
"enterHint": "एंटर भेजें · शिफ्ट+एंटर नई पंक्ति"
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (बीटा के दौरान लॉगिन आवश्यक, मुफ्त खेल)' : '';
|
||||
return `अपने विचार दर्ज करें, शैलियों को कॉन्फ़िगर करें और खेलने के लिए "शुरू" क्लिक करें${authHint}। आप नीचे से एक क्यूरेटेड कहानी चुनकर <em>InfiPlot</em> का तेजी से अनुभव भी कर सकते हैं। "सेटिंग्स" पर क्लिक करें अपना नाम दर्ज करने और अपनी टेक्स्ट, इमेज, विजन और TTS कुंजियों को कॉन्फ़िगर करने के लिए—सब कुछ अधिक स्थिर अनुभव के लिए आपके ब्राउज़र में स्थानीय रूप से संग्रहीत है।`;
|
||||
},
|
||||
"closeAriaLabel": "यह संकेत फिर न दिखाएं"
|
||||
},
|
||||
"about": {
|
||||
"title": "InfiPlot",
|
||||
"description": "एक इंटरैक्टिव स्टोरी गेम है जो AI का उपयोग करके रीयल-टाइम में सामग्री उत्पन्न करता है — चित्र, आवाज और कथा शाखाएं खेल के दौरान तुरंत उत्पन्न होती हैं।",
|
||||
"team": "टीम",
|
||||
"teamText": "हम सिंघुआ विश्वविद्यालय, लांज़ू विश्वविद्यालय और अन्य संस्थानों से आते हैं, और हम बहु-मोडल मॉडल की संभावनाओं का पता लगाना चाहते हैं। यह परियोजना अभी प्रारंभिक चरण में है, हम अभी भी सदस्यों की तलाश में हैं। यदि आप भी रुचि रखते हैं, तो कृपया संपर्क करें, हम आपके शामिल होने की प्रतीक्षा करते हैं।",
|
||||
"contact": "संपर्क",
|
||||
"email": "ईमेल",
|
||||
"openSource": "ओपन सोर्स पता",
|
||||
"betaUsers": "बीटा उपयोगकर्ता समूह",
|
||||
"qqGroupLabel": "QQ समूह नंबर:",
|
||||
"qqGroupAlt": "InfiPlot सार्वजनिक बीटा समूह QR कोड (समूह नंबर 575404333)",
|
||||
"privacyPolicy": "गोपनीयता नीति",
|
||||
"terms": "सेवा की शर्तें",
|
||||
"copyright": "© 2026 InfiPlot. सर्वाधिकार सुरक्षित।"
|
||||
},
|
||||
"errors": {
|
||||
"emptyFile": "यह कहानी फ़ाइल खाली है।",
|
||||
"fileTooLarge": "कहानी फ़ाइल बहुत बड़ी है, लोड नहीं हो सकती।",
|
||||
"unpackFailed": "कहानी फ़ाइल अनपैक करने में विफल।",
|
||||
"parseFailed": "कहानी फ़ाइल पार्स करने में विफल।",
|
||||
"cardNotFound": "चयनित कहानी नहीं मिली: {cardName}"
|
||||
}
|
||||
},
|
||||
"play": {
|
||||
"loading": {
|
||||
"firstFrame": "प्रथम दृश्य बन रहा है",
|
||||
"transitioning": "AI अगला दृश्य बना रहा है",
|
||||
"visionThinking": "AI सोच रहा है आपने क्या देखा",
|
||||
"loadingFirst": "पहला दृश्य लोड हो रहा है",
|
||||
"awakening": "लोड हो रहा है"
|
||||
},
|
||||
"freeform": {
|
||||
"placeholder": "आप जो कहना या करना चाहते हैं वह टाइप करें...",
|
||||
"title": "स्वतंत्र इनपुट",
|
||||
"ariaLabel": "स्वतंत्र इनपुट"
|
||||
},
|
||||
"choiceDisabled": "साझा कहानी में यह शाखा शामिल नहीं है",
|
||||
"tooltips": {
|
||||
"openSettings": "सेटिंग्स खोलें",
|
||||
"openHistory": "कहानी इतिहास",
|
||||
"fullscreen": "फुलस्क्रीन (F)",
|
||||
"enterFullscreen": "फुलस्क्रीन में प्रवेश करें",
|
||||
"exportGallery": "इंटरैक्टिव गैलरी लिंक के रूप में निर्यात करें",
|
||||
"exportGalleryLabel": "इंटरैक्टिव गैलरी निर्यात करें",
|
||||
"shareStory": "चालू कहानी .infiplot के रूप में निर्यात करें",
|
||||
"shareStoryLabel": "वर्तमान कहानी साझा करें",
|
||||
"mute": "मूक",
|
||||
"unmute": "आवाज़ चालू",
|
||||
"closeNudge": "संकेत बंद करें",
|
||||
"silenceNudge": "प्रभाव संतोषजनक नहीं/अक्सर कोई आवाज़ नहीं? अपना API कुंजी आज़माएं",
|
||||
"back": "वापस"
|
||||
},
|
||||
"imageAlt": "उत्पन्न दृश्य",
|
||||
"counter": {
|
||||
"scene": "दृश्य {n}",
|
||||
"beat": "बीट {n}",
|
||||
"middle": "·"
|
||||
},
|
||||
"buttons": {
|
||||
"fullscreen": "F · कुंजी · फुलस्क्रीन",
|
||||
"exportGallery": "गैलरी · निर्यात",
|
||||
"shareStory": "कहानी · साझा",
|
||||
"muted": "मूक",
|
||||
"sound": "आवाज़"
|
||||
},
|
||||
"error": {
|
||||
"title": "कुछ समस्या आई",
|
||||
"back": "वापस"
|
||||
},
|
||||
"previousStep": "पिछला चरण",
|
||||
"settingsFooter": "सहेजने के बाद TTS कुंजी तुरंत प्रभावी होगी, अपने कोटे से वर्तमान दृश्य की आवाज़ बनाएं।",
|
||||
"shareErrors": {
|
||||
"notFound": "लोड करने के लिए कोई कहानी फ़ाइल नहीं मिली।",
|
||||
"invalid": "कहानी साझा फ़ाइल में कोई लोड करने योग्य कहानी नहीं है।",
|
||||
"noImage": "कहानी साझा फ़ाइल में पहला दृश्य चित्र नहीं है।",
|
||||
"noNextImage": "कहानी साझा फ़ाइल में अगला दृश्य चित्र नहीं है।",
|
||||
"noMemory": "कहानी साझा फ़ाइल में प्रारंभिक कहानी स्मृति नहीं है।",
|
||||
"packFailed": "कहानी साझा पैकेजिंग विफल"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "सेटिंग्स",
|
||||
"subtitle": "वैकल्पिक · ये सेटिंग्स केवल स्थानीय ब्राउज़र में सहेजी जाती हैं",
|
||||
"tabs": {
|
||||
"general": "सामान्य",
|
||||
"models": "मॉडल"
|
||||
},
|
||||
"general": {
|
||||
"playerName": "खिलाड़ी का नाम",
|
||||
"playerNamePlaceholder": "खाली छोड़ने पर 'आप' का उपयोग होगा",
|
||||
"playerNameHint": "NPC बातचीत में इस नाम से संबोधित करेंगे।",
|
||||
"visionClick": "दृश्य पर क्लिक पहचान",
|
||||
"visionOn": "चालू",
|
||||
"visionOff": "बंद",
|
||||
"visionHint": "चालू करने पर, चयन नोड पर दृश्य क्लिक करने से AI दृश्य पहचान और नई कहानी शाखा उत्पन्न होगी।"
|
||||
},
|
||||
"models": {
|
||||
"corsNotice": "सुनिश्चित करें कि आपका API एंडपॉइंट ब्राउज़र CORS अनुरोध का समर्थन करता है। अधिकांश प्रमुख प्रदाता (OpenAI, Anthropic, Gemini, Runware आदि) पहले से समर्थन करते हैं।",
|
||||
"textModel": "पाठ मॉडल",
|
||||
"imageModel": "चित्र मॉडल",
|
||||
"visionModel": "दृश्य मॉडल",
|
||||
"baseUrl": "आधार URL",
|
||||
"apiKey": "API कुंजी",
|
||||
"model": "मॉडल",
|
||||
"provider": "प्रदाता (वैकल्पिक)",
|
||||
"providerHint": "खाली छोड़ने पर सिस्टम आधार URL से स्वचालित रूप से प्रोटोकॉल निर्धारित करेगा।",
|
||||
"providerAuto": "स्वचालित अनुमान (अनुशंसित)",
|
||||
"show": "दिखाएं",
|
||||
"hide": "छुपाएं"
|
||||
},
|
||||
"tts": {
|
||||
"title": "आवाज़ डबिंग मॉडल",
|
||||
"description": 'अपना <span class="text-clay-800">शाओमी MiMo API कुंजी</span> भरें, डबिंग ब्राउज़र में स्थानीय रूप से संश्लेषित होगी, कुंजी केवल स्थानीय रूप से सहेजी जाती है। MiMo TTS वर्तमान में<span class="text-clay-800">मुफ्त</span> है।',
|
||||
"keyType": "कुंजी प्रकार",
|
||||
"payg": "भुगतान-जैसा-आप-उपयोग-करें",
|
||||
"paygSub": "sk- से शुरू",
|
||||
"tokenPlan": "टोकन योजना",
|
||||
"tokenPlanSub": "tp- से शुरू",
|
||||
"region": "क्षेत्र नोड",
|
||||
"regionHint": "अपनी योजना सदस्यता क्षेत्र के साथ मेल खाता नोड चुनें।",
|
||||
"apiKeyPlaceholderPayg": "sk- से शुरू होने वाली कुंजी चिपकाएं",
|
||||
"apiKeyPlaceholderToken": "tp- से शुरू होने वाली कुंजी चिपकाएं",
|
||||
"keyMismatchPayg": "यह कुंजी sk- से शुरू नहीं होती",
|
||||
"keyMismatchToken": "यह कुंजी tp- से शुरू नहीं होती",
|
||||
"tutorialLink": "मुफ्त कुंजी कैसे प्राप्त करें? ट्यूटोरियल देखें"
|
||||
},
|
||||
"actions": {
|
||||
"save": "सहेजें",
|
||||
"clearAll": "सभी साफ़ करें"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"steps": {
|
||||
"pick": "जारी रखने के लिए लॉग इन करें",
|
||||
"email": "ईमेल लॉग इन",
|
||||
"otp": "सत्यापन कोड"
|
||||
},
|
||||
"googleLogin": "Google लॉग इन",
|
||||
"githubLogin": "GitHub लॉग इन",
|
||||
"emailLogin": "ईमेल सत्यापन कोड लॉग इन",
|
||||
"or": "या",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"sendCode": "कोड भेजें",
|
||||
"sending": "भेजा जा रहा है...",
|
||||
"codeSent": "सत्यापन कोड {email} पर भेजा गया",
|
||||
"codePlaceholder": "6 अंकीय सत्यापन कोड",
|
||||
"verify": "पुष्टि करें",
|
||||
"verifying": "सत्यापन हो रहा है...",
|
||||
"resend": "पुनः भेजें",
|
||||
"back": "वापस",
|
||||
"close": "बंद करें",
|
||||
"ariaLabel": "लॉग इन"
|
||||
},
|
||||
"history": {
|
||||
"title": "कथा · इतिहास",
|
||||
"close": "बंद करें",
|
||||
"closeAriaLabel": "कथा इतिहास बंद करें",
|
||||
"noHistory": "अभी कोई इतिहास नहीं है।",
|
||||
"scene": "दृश्य {n}",
|
||||
"choice": "चयन",
|
||||
"action": "कार्य",
|
||||
"ariaLabel": "कथा इतिहास"
|
||||
},
|
||||
"customForm": {
|
||||
"world": "दुनिया · दृष्टिकोण",
|
||||
"style": "शैली · चित्र शैली",
|
||||
"worldPlaceholder": "उदाहरण: 1990 के दशक के अंत में दक्षिणी चीन का एक छोटा शहर। मुख्य पात्र एक तीसरी वर्ष का स्थानांतरित छात्र है, जो बारिश वाले जून में छत पर कविता पढ़ने वाले एक सहपाठी से मिलता है।",
|
||||
"stylePlaceholder": "उदाहरण: जल रंग कोमल प्रकाश, दोपहर की गर्मी, एनीमे दृश्य उपन्यास शैली...",
|
||||
"status": {
|
||||
"ready": "तैयार · हो · गया",
|
||||
"needMore": "दो · अनुच्छेद · पर्याप्त",
|
||||
"starting": "पहला दृश्य लोड हो रहा है..."
|
||||
},
|
||||
"start": "शुरू करें"
|
||||
},
|
||||
"language": {
|
||||
"title": "भाषा",
|
||||
"current": "वर्तमान भाषा",
|
||||
"select": "भाषा चुनें"
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type hiTranslations = typeof hi;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Indonesian
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const id = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (login diperlukan selama beta, main gratis)' : '';
|
||||
return `Masukkan ide Anda, konfigurasi gaya, dan klik "Mulai" untuk bermain${authHint}. Anda juga dapat memilih cerita kurasi dari bawah untuk pengalaman cepat <em>InfiPlot</em>. Klik "Pengaturan" untuk memasukkan nama Anda dan mengonfigurasi kunci teks, gambar, visi, dan TTS Anda sendiri—semua disimpan secara lokal di browser Anda untuk pengalaman yang lebih stabil.`;
|
||||
},
|
||||
"closeAriaLabel": "Jangan tampilkan petunjuk ini lagi"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type IdTranslations = typeof id;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Italian
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const it = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (accesso richiesto durante la beta, gioco gratuito)' : '';
|
||||
return `Inserisci le tue idee, configura gli stili e fai clic su "Inizia" per giocare${authHint}. Puoi anche scegliere una storia curata qui sotto per provare rapidamente <em>InfiPlot</em>. Fai clic su "Impostazioni" per inserire il tuo nome e configurare le tue chiavi di testo, immagine, visione e TTS—tutto salvato localmente nel tuo browser per un'esperienza più stabile.`;
|
||||
},
|
||||
"closeAriaLabel": "Non mostrare più questo suggerimento"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type ItTranslations = typeof it;
|
||||
@@ -0,0 +1,426 @@
|
||||
// Japanese — auto-translated from zh-CN by scripts/translate-i18n.mjs (review for quality).
|
||||
// Extracted from components: page.tsx, layout.tsx, CustomForm.tsx, SettingsModal.tsx, PlayCanvas.tsx, AuthModal.tsx, DialogueHistoryModal.tsx
|
||||
|
||||
export const ja = {
|
||||
// ========== Layout ==========
|
||||
layout: {
|
||||
metadata: {
|
||||
title: "InfiPlot — AIリアルタイムインタラクティブストーリーゲーム",
|
||||
description: "InfiPlotは、AIを用いて画像、音声、ストーリー分岐をリアルタイムに生成するインタラクティブ・ストーリーゲームのデモです。",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Home Page (page.tsx) ==========
|
||||
home: {
|
||||
// Example phrases for typewriter
|
||||
examples: {
|
||||
male: [
|
||||
"幼い頃から一緒に育った幼馴染が、突然顔を赤くして私に告白してきた",
|
||||
"目が覚めたら、クラスの女子たちがみんな密かに俺のことを好きになっているみたいだ",
|
||||
"三年の期は満ちた。実は私が御曹司だったとは。復讐の時が来た。",
|
||||
"無限のTokenを手に、インターネット誕生の前夜へとタイムスリップした……",
|
||||
],
|
||||
female: [
|
||||
"将軍家の落ちこぼれ嫡女に転生したのに、冷徹な摂政王は私だけを溺愛する",
|
||||
"別れの前夜に巻き戻り、今度は私から手を放す",
|
||||
"目が覚めたら乙女ゲームの悪役令嬢になっていた。すべての死亡エンドを回避しなくては",
|
||||
],
|
||||
x: [
|
||||
"時空の裂け目が開き、複数の平行世界の自分が突如目の前に現れた",
|
||||
"記憶の宮殿で、忘れ去られた断片が新たな物語へと再構成されている。",
|
||||
"無限流ゲームが始まる。全員に与えられたクリアの機会は、ただ一度きり。",
|
||||
"システム提示:あなたの選択が全宇宙の運命を左右します。",
|
||||
],
|
||||
},
|
||||
|
||||
// Option labels
|
||||
options: {
|
||||
gender: "性的指向",
|
||||
artStyle: "画風",
|
||||
plotStyle: "シナリオスタイル",
|
||||
voice: "ボイス",
|
||||
pacing: "コンテンツのペース",
|
||||
},
|
||||
|
||||
// Option values - genders
|
||||
genders: {
|
||||
male: "男性向け",
|
||||
female: "女性向け",
|
||||
x: "X",
|
||||
},
|
||||
|
||||
// Option values - art styles
|
||||
artStyles: {
|
||||
auto: "オート",
|
||||
custom: "カスタムスタイル",
|
||||
kyoani: "京アニ",
|
||||
shinkai: "新海誠",
|
||||
ghibli: "ジブリ",
|
||||
"3d": "3Dアニメーション",
|
||||
cyberpunk: "サイバーパンク",
|
||||
gothic: "ゴシック",
|
||||
wasteland: "ポストアポカリプス",
|
||||
pixel: "ドット絵風",
|
||||
realistic: "現実",
|
||||
oil: "古典油絵",
|
||||
monet: "モネ",
|
||||
watercolor: "水彩",
|
||||
ink: "水墨",
|
||||
ukiyoe: "浮世絵",
|
||||
pencil: "色鉛筆",
|
||||
sketch: "手描きスケッチ",
|
||||
manga: "モノクロ漫画",
|
||||
children: "子ども向け絵本",
|
||||
crayon: "子どもの落書き",
|
||||
clay: "粘土細工",
|
||||
dunhuang: "敦煌壁画",
|
||||
miniature: "細密画",
|
||||
mosaic: "モザイク画",
|
||||
stainedGlass: "ステンドグラス",
|
||||
vaporwave: "ヴェイパーウェイヴ",
|
||||
vector: "ベクターイラスト",
|
||||
lowpoly: "ローポリゴン",
|
||||
popart: "ポップアート",
|
||||
glitch: "グリッチアート",
|
||||
papercut: "切り絵",
|
||||
steampunk: "スチームパンク",
|
||||
xianxia: "仙侠ファンタジー",
|
||||
darkFairytale: "暗黒童話",
|
||||
urbanFantasy: "都市幻想",
|
||||
},
|
||||
|
||||
// Option values - plot styles
|
||||
plotStyles: {
|
||||
straightforward: "ストレートな展開",
|
||||
twist: "複数ルート分岐",
|
||||
suspense: "サスペンス",
|
||||
healing: "癒やし系日常",
|
||||
},
|
||||
|
||||
// Option values - voice
|
||||
voiceOptions: {
|
||||
off: "オフ",
|
||||
on: "オン",
|
||||
},
|
||||
|
||||
// Option values - pacing
|
||||
pacings: {
|
||||
slow: "じっくり繊細",
|
||||
fast: "テンポよく",
|
||||
},
|
||||
|
||||
// Story cards (samples - in production these would come from presets.ts)
|
||||
stories: {
|
||||
// A few representative titles
|
||||
贤者陨落: "賢者の終焉",
|
||||
画中圣手: "画中の名手",
|
||||
花魁的刀: "花魁の刀",
|
||||
// ... (full list would be presets.ts stories)
|
||||
},
|
||||
|
||||
// UI labels
|
||||
ui: {
|
||||
start: "スタート",
|
||||
loadStory: "シナリオ読み込み",
|
||||
settings: "設定",
|
||||
searchPlaceholder: "スタイルを検索…",
|
||||
noMatchingStyle: "一致するスタイルがありません",
|
||||
close: "閉じる",
|
||||
back: "戻る",
|
||||
save: "保存",
|
||||
cancel: "キャンセル",
|
||||
saveAndSelect: "保存して適用",
|
||||
},
|
||||
|
||||
// Style modal
|
||||
styleModal: {
|
||||
title: "画風を選択",
|
||||
subtitle: 'デフォルトは「自動」で、AIがストーリーに基づいて画風を自動的にマッチングします。「カスタムスタイル」を選択すると、説明の入力や参考画像のアップロードが可能です。',
|
||||
customTitle: "カスタムスタイル",
|
||||
customPlaceholder: `希望する画像スタイルを入力してください。例えば:
|
||||
幻想的な水彩画風、柔らかな色調、ノスタルジックな雰囲気
|
||||
|
||||
💡 ヒント:一部の画像生成モデルは英語のプロンプトの方が効果が高いため、事前にAIチャットツール等で専門的な英語のスタイル記述を生成し、ここに貼り付けることをお勧めします。`,
|
||||
uploadImage: "参考画像をアップロード",
|
||||
changeImage: "別の画像にする",
|
||||
remove: "削除",
|
||||
parsing: "解析中…",
|
||||
importFromPreset: "プリセットスタイルからインポート…",
|
||||
uploadError: "画像ファイルのみ対応しています",
|
||||
visionError: "視覚モデルが空のスタイル説明を返しました",
|
||||
fileReadError: "ファイルの読み込みに失敗しました",
|
||||
imageDecodeError: "画像をデコードできません",
|
||||
parseError: "解析に失敗しました",
|
||||
refImageAlt: "画風参考画像",
|
||||
},
|
||||
|
||||
// Hero section
|
||||
hero: {
|
||||
title: "今日はどんな物語を体験したいですか?",
|
||||
placeholder: " ",
|
||||
enterHint: "Enterで送信 Shift+Enterで改行",
|
||||
},
|
||||
|
||||
// Usage hint
|
||||
hint: {
|
||||
text: (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? '(テスト期間中、ログインするだけで無料でプレイできます)' : '';
|
||||
return `アイデアを入力し、スタイルを設定して、「開始」をクリックするだけでプレイできます${authHint}。また、下の厳選ストーリー集から1つ選んで、すぐに <em class="not-italic text-ember-500">InfiPlot</em> を体験することもできます。「<span class="inline-flex items-center gap-1 text-ember-500"><i class="fa-solid fa-gear text-[10px]"></i>設定</span>」をクリックすると、あなたの名前や、ご自身のテキスト、画像生成、画像認識モデル、そしてボイス Key を入力することもできます。これらはすべてローカルブラウザにのみ保存されるため、より安定して体験できます。`;
|
||||
},
|
||||
closeAriaLabel: "今後このメッセージを表示しない",
|
||||
},
|
||||
|
||||
// About section
|
||||
about: {
|
||||
title: "InfiPlot",
|
||||
description: "AIでコンテンツをリアルタイムに生成するインタラクティブ・ストーリーゲームです——画像、音声、ストーリーの分岐がプレイ中にその場で生成されます。",
|
||||
team: "チーム",
|
||||
teamText: "私たちは清華大学や蘭州大学などの大学の出身で、マルチモーダルモデルにおける「画像や動画の直接生成」といったoneshot機能の枠を超えた、さらなる可能性を模索しています。本プロジェクトは現在まだ初期段階にあり、メンバーを募集中です。もしご興味がございましたら、ぜひご連絡ください。皆様のご参加を心よりお待ちしております。",
|
||||
contact: "連絡先",
|
||||
email: "メールアドレス",
|
||||
openSource: "ソースコード",
|
||||
betaUsers: "クローズドβユーザーグループ",
|
||||
qqGroupLabel: "QQグループ番号:",
|
||||
qqGroupAlt: "InfiPlot オープンβ交流QQグループ QRコード(グループ番号 575404333)",
|
||||
legalNotice: (params: { analyticsOn?: boolean }) => {
|
||||
const base = `公開テスト期間中、本製品は無料でご利用いただけますが、同時接続ユーザー数によって動作の安定性が変動する場合があります。<br />公開テスト期間中に生成されたコンテンツはサーバーに保存されません。保存が必要な場合は、プレイ終了後に図集のエクスポートまたはストーリー共有機能を使用して、プレイ体験を保存してください。<br />AIによって生成されたコンテンツは、当チームの立場を代表するものではありません。`;
|
||||
if (params.analyticsOn) {
|
||||
return `${base}<br />当サイトは、オープンソースの <a href="https://umami.is/" target="_blank" rel="noopener noreferrer">Umami</a> を使用して、プライバシーに配慮した匿名のアクセスおよびインタラクション統計を行っています:Cookieは使用せず、個人情報は収集せず、入力された内容は一切送信せず、クロスサイトトラッキングも行いません。`;
|
||||
}
|
||||
return base;
|
||||
},
|
||||
privacyPolicy: "プライバシーポリシー",
|
||||
terms: "利用規約",
|
||||
copyright: "© 2026 InfiPlot. All rights reserved.",
|
||||
},
|
||||
|
||||
// Story import errors
|
||||
errors: {
|
||||
emptyFile: "このシナリオファイルは空です。",
|
||||
fileTooLarge: "シナリオファイルが大きすぎるため、ロードできません。",
|
||||
unpackFailed: "シナリオファイルのアンパックに失敗しました。",
|
||||
parseFailed: "シナリオファイルの解析に失敗しました。",
|
||||
cardNotFound: "おすすめストーリーが見つかりません:{cardName}",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Play Page (PlayCanvas.tsx & play/page.tsx) ==========
|
||||
play: {
|
||||
// Loading states
|
||||
loading: {
|
||||
firstFrame: "第一幕を描画中",
|
||||
transitioning: "AIが次の幕を描画中",
|
||||
visionThinking: "AIはあなたが何を見たか考えています",
|
||||
loadingFirst: "第一幕を起動中",
|
||||
awakening: "ロード中",
|
||||
},
|
||||
|
||||
// Freeform input
|
||||
freeform: {
|
||||
placeholder: "言いたいことややりたいことを入力...",
|
||||
title: "自由入力",
|
||||
ariaLabel: "自由入力",
|
||||
},
|
||||
|
||||
// Choice disabled title
|
||||
choiceDisabled: "共有されたストーリーにこの分岐は含まれていません",
|
||||
|
||||
// Tooltips
|
||||
tooltips: {
|
||||
openSettings: "設定を開く",
|
||||
openHistory: "シナリオ巻き戻し",
|
||||
fullscreen: "フルスクリーン (F)",
|
||||
enterFullscreen: "全画面表示",
|
||||
exportGallery: "このプレイをインタラクティブギャラリーのリンクとしてエクスポート(ボイス付き。直近2回分のリンクのみが保持されます)",
|
||||
exportGalleryLabel: "インタラクティブな図表をエクスポート",
|
||||
shareStory: "このプレイを続きからプレイ可能なシナリオ .infiplot(ボイス付き)としてエクスポート",
|
||||
shareStoryLabel: "現在のストーリーをシェア",
|
||||
mute: "ミュート",
|
||||
unmute: "ミュート解除",
|
||||
closeNudge: "ヒントを閉じる",
|
||||
silenceNudge: "効果に満足できない/よく音が出ない?ご自身の API Key を入力してみてください",
|
||||
back: "戻る",
|
||||
},
|
||||
|
||||
// Image alt
|
||||
imageAlt: "Generated scene",
|
||||
|
||||
// Scene/beat counter
|
||||
counter: {
|
||||
scene: "第 {n} 幕",
|
||||
beat: "{n} 拍",
|
||||
middle: " ",
|
||||
},
|
||||
|
||||
// Button labels
|
||||
buttons: {
|
||||
fullscreen: "Fキーで全画面",
|
||||
exportGallery: "図集のエクスポート",
|
||||
shareStory: "ストーリーを共有",
|
||||
muted: "消音",
|
||||
sound: "ボイスあり",
|
||||
},
|
||||
|
||||
// Error state
|
||||
error: {
|
||||
title: "問題が発生しました",
|
||||
back: "戻る",
|
||||
},
|
||||
|
||||
// Previous action
|
||||
previousStep: "前のアクション",
|
||||
|
||||
// Settings footer note
|
||||
settingsFooter: "保存後、ボイス Key はすぐに有効になり、ご自身のクレジットを使用して現在のシーンのボイスを合成します。",
|
||||
|
||||
// Share file errors
|
||||
shareErrors: {
|
||||
notFound: "読み込むシナリオファイルが見つかりませんでした。",
|
||||
invalid: "シナリオ共有ファイルにロード可能なシナリオがありません。",
|
||||
noImage: "シナリオ共有ファイルに第一幕の画像がありません。",
|
||||
noNextImage: "シナリオ共有ファイルに次のシーンの画像が不足しています。",
|
||||
noMemory: "シナリオ共有ファイルに初期シナリオ記憶が不足しているため、ロードできません。",
|
||||
packFailed: "シナリオ共有のパッケージ化に失敗しました",
|
||||
},
|
||||
|
||||
// Export progress
|
||||
exportProgress: {
|
||||
preparingVoice: "ボイスを準備中",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Settings Modal (SettingsModal.tsx) ==========
|
||||
settings: {
|
||||
title: "設定",
|
||||
subtitle: "任意:これらの設定はローカルブラウザにのみ保存されます",
|
||||
|
||||
// Tabs
|
||||
tabs: {
|
||||
general: "一般",
|
||||
models: "モデル",
|
||||
},
|
||||
|
||||
// General tab
|
||||
general: {
|
||||
playerName: "プレイヤー名",
|
||||
playerNamePlaceholder: "未入力の場合は「あなた」を使用します",
|
||||
playerNameHint: "NPCは会話の中でこの名前であなたを呼びます。入力しない場合はデフォルトで「あなた」と呼びます。",
|
||||
visionClick: "画面をクリックして認識",
|
||||
visionOn: "有効にする",
|
||||
visionOff: "閉じる",
|
||||
visionHint: "有効にすると、選択ノードで画面をクリックした際にAI画像認識がトリガーされ、新しいシナリオ分岐が生成されます。",
|
||||
},
|
||||
|
||||
// Models tab
|
||||
models: {
|
||||
corsNotice: "お使いのAPIエンドポイントがブラウザのクロスオリジン要求(CORS)をサポートしていることを確認してください。ほとんどの主要プロバイダー(OpenAI、Anthropic、Gemini、Runwareなど)は、すでにデフォルトでサポートしています。",
|
||||
textModel: "テキストモデル",
|
||||
imageModel: "描画モデル",
|
||||
visionModel: "画像認識モデル",
|
||||
baseUrl: "BASE URL",
|
||||
apiKey: "API Key",
|
||||
model: "Model",
|
||||
provider: "プロバイダー(任意)",
|
||||
providerHint: "空欄の場合、システムは Base URL に基づいてプロトコルを自動的に推測します。",
|
||||
providerAuto: "自動判定(推奨)",
|
||||
show: "表示",
|
||||
hide: "非表示",
|
||||
},
|
||||
|
||||
// TTS section
|
||||
tts: {
|
||||
title: "ボイスモデル",
|
||||
description: 'ご自身の <span class="text-clay-800">Xiaomi MiMo API Key</span> を入力すると、ボイスはブラウザのローカルで合成されます。Keyはローカルにのみ保存され、サーバーを経由することはありません。MiMo TTSは現在<span class="text-clay-800">期間限定で無料</span>となっており、申請すればすぐに使用できます。',
|
||||
keyType: "Key タイプ",
|
||||
payg: "従量課金",
|
||||
paygSub: "sk-で始まる",
|
||||
tokenPlan: "トークンプラン",
|
||||
tokenPlanSub: "tp- で始まる",
|
||||
region: "エリアノード",
|
||||
regionHint: "ご契約プランの地域と一致するノードを選択してください(通常、最も遅延が少ないノードです)。",
|
||||
apiKeyPlaceholderPayg: "sk-で始まる従量課金 Key を貼り付け",
|
||||
apiKeyPlaceholderToken: "tp-で始まるプランKeyを貼り付け",
|
||||
keyMismatchPayg: 'このKeyはsk-で始まっていません。選択した「従量課金 Pay-as-you-go」タイプと一致しない可能性があります。入力内容に誤りがないかご確認ください。',
|
||||
keyMismatchToken: 'この Key は tp- で始まっていないため、選択された「プラン Token Plan」のタイプと一致しない可能性があります。入力内容に誤りがないかご確認ください。',
|
||||
tutorialLink: "無料でKeyを申請するには?図解チュートリアルを見る",
|
||||
},
|
||||
|
||||
// Actions
|
||||
actions: {
|
||||
save: "保存",
|
||||
clearAll: "すべてクリア",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Auth Modal (AuthModal.tsx) ==========
|
||||
auth: {
|
||||
// Steps
|
||||
steps: {
|
||||
pick: "ログインして続行",
|
||||
email: "メールアドレスでログイン",
|
||||
otp: "認証コード",
|
||||
},
|
||||
|
||||
// Buttons
|
||||
googleLogin: "Google ログイン",
|
||||
githubLogin: "GitHubでログイン",
|
||||
emailLogin: "メール認証コードでログイン",
|
||||
or: "または",
|
||||
|
||||
// Email input
|
||||
emailPlaceholder: "your@email.com",
|
||||
sendCode: "認証コードを送信",
|
||||
sending: "送信中...",
|
||||
|
||||
// OTP verification
|
||||
codeSent: "認証コードを{email}に送信しました",
|
||||
codePlaceholder: "6桁の認証コード",
|
||||
verify: "確認",
|
||||
verifying: "検証中...",
|
||||
resend: "再送信",
|
||||
|
||||
// Navigation
|
||||
back: "戻る",
|
||||
|
||||
// Close
|
||||
close: "閉じる",
|
||||
|
||||
// Aria labels
|
||||
ariaLabel: "ログイン",
|
||||
},
|
||||
|
||||
// ========== Dialogue History Modal ==========
|
||||
history: {
|
||||
title: "シナリオ回想",
|
||||
close: "閉じる",
|
||||
closeAriaLabel: "シナリオ回想を閉じる",
|
||||
noHistory: "履歴はありません。",
|
||||
scene: "第 {n} 幕",
|
||||
choice: "選択",
|
||||
action: "行動",
|
||||
ariaLabel: "シナリオ巻き戻し",
|
||||
},
|
||||
|
||||
// ========== Custom Form (CustomForm.tsx) ==========
|
||||
customForm: {
|
||||
world: "世界観",
|
||||
style: "画風",
|
||||
worldPlaceholder: "例:1990年代末の中国南部の地方都市。主人公は高校3年生の転校生。雨の多い6月に、いつも屋上で詩を読んでいる同級生と出会う。ストーリーはスロースタートで、控えめ、どこか切ない…",
|
||||
stylePlaceholder: "例:水彩風の柔らかな光、午後の温もり、アニメ風ビジュアルノベル画風、従来の会話パネル…",
|
||||
status: {
|
||||
ready: "準備完了",
|
||||
needMore: "2つの段落でスタート",
|
||||
starting: "最初のフレームを呼び出し中…",
|
||||
},
|
||||
start: "スタート",
|
||||
},
|
||||
|
||||
// ========== Language Switcher ==========
|
||||
language: {
|
||||
title: "言語",
|
||||
current: "現在の言語",
|
||||
select: "言語の選択",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type JaTranslations = typeof ja;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Korean
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const ko = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? '(베타 기간 중, 로그인하면 무료 플레이)' : '';
|
||||
return `아이디어를 입력하고 스타일을 구성한 후 "시작"을 클릭하여 플레이${authHint}. 또는 아래의 큐레이션된 스토리 중 하나를 선택하여 <em>InfiPlot</em>을 빠르게 경험할 수도 있습니다. "설정"을 클릭하여 이름과 텍스트, 이미지, 비전 모델, TTS 키를 입력할 수 있습니다—모두 브라우저에 로컬로 저장되어 더 안정적인 경험을 제공합니다.`;
|
||||
},
|
||||
"closeAriaLabel": "이 힌트를 다시 표시하지 않음"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type KoTranslations = typeof ko;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Dutch
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const nl = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (inloggen vereist tijdens beta, gratis spelen)' : '';
|
||||
return `Voer je ideeën in, configureer stijlen en klik op "Starten" om te spelen${authHint}. Je kunt ook een gecureerd verhaal onderaan kiezen om <em>InfiPlot</em> snel te ervaren. Klik op "Instellingen" om je naam in te voeren en je eigen tekst-, afbeeldings-, visie- en TTS-sleutels te configureren—alles lokaal in je browser opgeslagen voor een stabielere ervaring.`;
|
||||
},
|
||||
"closeAriaLabel": "Deze hint niet meer weergeven"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type NlTranslations = typeof nl;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Polish
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const pl = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (wymagane logowanie podczas beta, darmowa gra)' : '';
|
||||
return `Wprowadź swoje pomysły, skonfiguruj style i kliknij "Rozpocznij", aby zagrać${authHint}. Możesz także wybrać kuratorską historię z dołu, aby szybko doświadczyć <em>InfiPlot</em>. Kliknij "Ustawienia", aby wprowadzić swoje imię i skonfigurować własne klucze tekstu, obrazu, widoku i TTS—wszystko przechowywane lokalnie w twojej przeglądarce dla bardziej stabilnego doświadczenia.`;
|
||||
},
|
||||
"closeAriaLabel": "Nie pokazuj więcej tej podpowiedzi"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type PlTranslations = typeof pl;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Portuguese (Brazil)
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const ptBR = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (login necessário durante o beta, grátis para jogar)' : '';
|
||||
return `Digite suas ideias, configure estilos e clique em "Iniciar" para jogar${authHint}. Você também pode escolher uma história curada abaixo para experimentar rapidamente <em>InfiPlot</em>. Clique em "Configurações" para inserir seu nome e configurar suas próprias chaves de texto, imagem, visão e TTS—tudo armazenado localmente no seu navegador para uma experiência mais estável.`;
|
||||
},
|
||||
"closeAriaLabel": "Não mostrar mais este aviso"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type PtBRTranslations = typeof ptBR;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Portuguese (Portugal)
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const pt = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (login necessário durante o beta, grátis para jogar)' : '';
|
||||
return `Digite as suas ideias, configure estilos e clique em "Iniciar" para jogar${authHint}. Também pode escolher uma história curada abaixo para experimentar rapidamente <em>InfiPlot</em>. Clique em "Configurações" para inserir o seu nome e configurar as suas próprias chaves de texto, imagem, visão e TTS—tudo guardado localmente no seu navegador para uma experiência mais estável.`;
|
||||
},
|
||||
"closeAriaLabel": "Não mostrar esta dica novamente"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type PtTranslations = typeof pt;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Russian
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const ru = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (требуется вход во время бета-теста, бесплатная игра)' : '';
|
||||
return `Введите свои идеи, настройте стили и нажмите "Начать" для игры${authHint}. Вы также можете выбрать выбранную историю ниже, чтобы быстро испытать <em>InfiPlot</em>. Нажмите "Настройки", чтобы ввести свое имя и настроить свои собственные ключи текста, изображения, зрения и TTS—все сохраняется локально в вашем браузере для более стабильного опыта.`;
|
||||
},
|
||||
"closeAriaLabel": "Больше не показывать эту подсказку"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type RuTranslations = typeof ru;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Thai
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const th = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (ต้องล็อกอินระหว่างเบต้า, เล่นฟรี)' : '';
|
||||
return `ป้อนแนวคิดของคุณ กำหนดค่าสไตล์ และคลิก "เริ่ม" เพื่อเล่น${authHint} คุณยังสามารถเลือกเรื่องราวที่คัดสรรจากด้านล่างเพื่อสัมผัส <em>InfiPlot</em> ได้อย่างรวดเร็ว คลิก "การตั้งค่า" เพื่อป้อนชื่อและกำหนดค่าคีย์ข้อความ รูปภาพ การมองเห็น และ TTS ของคุณเอง—ทั้งหมดจะถูกเก็บไว้ในเบราว์เซอร์ของคุณเพื่อประสบการณ์ที่มีเสถียรภาพมากขึ้น`;
|
||||
},
|
||||
"closeAriaLabel": "ไม่แสดงคำแนะนำนี้อีก"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type ThTranslations = typeof th;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Turkish
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const tr = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (beta sırasında giriş gerekli, ücretsiz oyun)' : '';
|
||||
return `Fikirlerinizi girin, stilleri yapılandırın ve oynamak için "Başlat"a tıklayın${authHint}. Aşağıdan küratörlü bir hikaye seçerek <em>InfiPlot</em>'ı hızlıca deneyimleyebilirsiniz. "Ayarlar"a tıklayarak adınızı girebilir ve kendi metin, resim, görü ve TTS anahtarlarınızı yapılandırabilirsiniz—tümü daha stabil bir deneyim için tarayıcınızda yerel olarak saklanır.`;
|
||||
},
|
||||
"closeAriaLabel": "Bu ipucunu bir daha gösterme"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type TrTranslations = typeof tr;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Ukrainian
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const uk = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (вхід потрібен під час бета-тестування, безкоштовна гра)' : '';
|
||||
return `Введіть свої ідеї, налаштуйте стилі та натисніть "Почати" для гри${authHint}. Ви також можете обрати вибрану історію знизу, щоб швидко випробувати <em>InfiPlot</em>. Натисніть "Налаштування", щоб ввести своє ім'я та налаштувати власні ключі тексту, зображення, зору та TTS—все зберігається локально у вашому браузері для стабільнішого досвіду.`;
|
||||
},
|
||||
"closeAriaLabel": "Більше не показувати цю підказку"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type UkTranslations = typeof uk;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Vietnamese
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const vi = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? ' (yêu cầu đăng nhập trong bản beta, chơi miễn phí)' : '';
|
||||
return `Nhập ý tưởng của bạn, cấu hình kiểu và nhấp "Bắt đầu" để chơi${authHint}. Bạn cũng có thể chọn một câu chuyện được chọn từ bên dưới để trải nghiệm nhanh <em>InfiPlot</em>. Nhấp "Cài đặt" để nhập tên của bạn và cấu hình khóa văn bản, hình ảnh, hình ảnh và TTS của riêng bạn—tất cả được lưu cục bộ trong trình duyệt của bạn để có trải nghiệm ổn định hơn.`;
|
||||
},
|
||||
"closeAriaLabel": "Không còn hiển thị gợi ý này"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type ViTranslations = typeof vi;
|
||||
@@ -0,0 +1,426 @@
|
||||
// Chinese (Simplified) - Source language
|
||||
// Extracted from components: page.tsx, layout.tsx, CustomForm.tsx, SettingsModal.tsx, PlayCanvas.tsx, AuthModal.tsx, DialogueHistoryModal.tsx
|
||||
|
||||
export const zhCN = {
|
||||
// ========== Layout ==========
|
||||
layout: {
|
||||
metadata: {
|
||||
title: "InfiPlot — AI 实时交互剧情游戏",
|
||||
description: "InfiPlot 是一款用 AI 实时生成图片、语音与剧情分支的交互式剧情游戏 Demo。",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Home Page (page.tsx) ==========
|
||||
home: {
|
||||
// Example phrases for typewriter
|
||||
examples: {
|
||||
male: [
|
||||
"从小一起长大的青梅竹马,突然红着脸向我告白",
|
||||
"一觉醒来,班上的女生好像都偷偷喜欢上了我",
|
||||
"三年之期已到,原来我是富家公子,报仇时机已到",
|
||||
"我带着无限 Token 穿越回了互联网诞生前夕……",
|
||||
],
|
||||
female: [
|
||||
"穿越成将军府的废物嫡女,冷面摄政王却独宠我一人",
|
||||
"重生回到分手前夜,这一次换我先放手",
|
||||
"一觉醒来成了乙游里的恶役千金,要躲开所有死亡结局",
|
||||
],
|
||||
x: [
|
||||
"时空裂隙开启,多个平行世界的自己突然出现在眼前",
|
||||
"记忆宫殿里,那些被遗忘的碎片正在重组为新的故事",
|
||||
"一场无限流游戏开始,所有人都有唯一的通关机会",
|
||||
"系统提示:你的选择将决定整个宇宙的命运走向",
|
||||
],
|
||||
},
|
||||
|
||||
// Option labels
|
||||
options: {
|
||||
gender: "性向",
|
||||
artStyle: "绘画风格",
|
||||
plotStyle: "剧情风格",
|
||||
voice: "语音配音",
|
||||
pacing: "内容节奏",
|
||||
},
|
||||
|
||||
// Option values - genders
|
||||
genders: {
|
||||
male: "男性向",
|
||||
female: "女性向",
|
||||
x: "X",
|
||||
},
|
||||
|
||||
// Option values - art styles
|
||||
artStyles: {
|
||||
auto: "自动",
|
||||
custom: "自定义风格",
|
||||
kyoani: "京阿尼",
|
||||
shinkai: "新海诚",
|
||||
ghibli: "吉卜力",
|
||||
"3d": "3D 动画",
|
||||
cyberpunk: "赛博朋克",
|
||||
gothic: "哥特",
|
||||
wasteland: "废土",
|
||||
pixel: "像素风",
|
||||
realistic: "真实",
|
||||
oil: "古典油画",
|
||||
monet: "莫奈",
|
||||
watercolor: "水彩",
|
||||
ink: "水墨",
|
||||
ukiyoe: "浮世绘",
|
||||
pencil: "彩铅",
|
||||
sketch: "手绘素描",
|
||||
manga: "黑白漫画",
|
||||
children: "儿童绘本",
|
||||
crayon: "儿童涂鸦",
|
||||
clay: "黏土手工",
|
||||
dunhuang: "敦煌壁画",
|
||||
miniature: "细密画",
|
||||
mosaic: "镶嵌画",
|
||||
stainedGlass: "彩绘玻璃",
|
||||
vaporwave: "蒸汽波",
|
||||
vector: "矢量插画",
|
||||
lowpoly: "低多边形",
|
||||
popart: "波普艺术",
|
||||
glitch: "故障艺术",
|
||||
papercut: "剪纸艺术",
|
||||
steampunk: "蒸汽朋克",
|
||||
xianxia: "仙侠玄幻",
|
||||
darkFairytale: "暗黑童话",
|
||||
urbanFantasy: "都市幻想",
|
||||
},
|
||||
|
||||
// Option values - plot styles
|
||||
plotStyles: {
|
||||
straightforward: "平铺直叙",
|
||||
twist: "多线转折",
|
||||
suspense: "悬疑烧脑",
|
||||
healing: "治愈日常",
|
||||
},
|
||||
|
||||
// Option values - voice
|
||||
voiceOptions: {
|
||||
off: "关闭",
|
||||
on: "开启",
|
||||
},
|
||||
|
||||
// Option values - pacing
|
||||
pacings: {
|
||||
slow: "慢热细腻",
|
||||
fast: "紧凑爽快",
|
||||
},
|
||||
|
||||
// Story cards (samples - in production these would come from presets.ts)
|
||||
stories: {
|
||||
// A few representative titles
|
||||
贤者陨落: "贤者陨落",
|
||||
画中圣手: "画中圣手",
|
||||
花魁的刀: "花魁的刀",
|
||||
// ... (full list would be presets.ts stories)
|
||||
},
|
||||
|
||||
// UI labels
|
||||
ui: {
|
||||
start: "开始",
|
||||
loadStory: "载入剧情",
|
||||
settings: "设置",
|
||||
searchPlaceholder: "搜索风格…",
|
||||
noMatchingStyle: "没有匹配的风格",
|
||||
close: "关闭",
|
||||
back: "返回",
|
||||
save: "保存",
|
||||
cancel: "取消",
|
||||
saveAndSelect: "保存并选用",
|
||||
},
|
||||
|
||||
// Style modal
|
||||
styleModal: {
|
||||
title: "选择绘画风格",
|
||||
subtitle: '默认「自动」· 由 AI 根据故事自动匹配画风;选择「自定义风格」可输入描述或上传参考图',
|
||||
customTitle: "自定义风格",
|
||||
customPlaceholder: `描述你想要的画面风格,例如:
|
||||
梦幻水彩风格,柔和的色调,怀旧的氛围
|
||||
|
||||
💡 提示:部分绘图模型对英文提示词效果更佳,建议先借助 AI 对话工具生成专业的英文风格描述,再粘贴到这里`,
|
||||
uploadImage: "上传参考图",
|
||||
changeImage: "换一张",
|
||||
remove: "移除",
|
||||
parsing: "解析中…",
|
||||
importFromPreset: "从预设风格导入…",
|
||||
uploadError: "只支持图片文件",
|
||||
visionError: "视觉模型返回了空的风格描述",
|
||||
fileReadError: "读取文件失败",
|
||||
imageDecodeError: "无法解码图片",
|
||||
parseError: "解析失败",
|
||||
refImageAlt: "画风参考图",
|
||||
},
|
||||
|
||||
// Hero section
|
||||
hero: {
|
||||
title: "今天想体验什么故事?",
|
||||
placeholder: " ",
|
||||
enterHint: "Enter 发送 · Shift+Enter 换行",
|
||||
},
|
||||
|
||||
// Usage hint
|
||||
hint: {
|
||||
text: (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? '(测试期间,登录即可免费畅玩)' : '';
|
||||
return `输入想法、配置风格,点击「开始」即可游玩${authHint};也可以从下方精选故事集挑一篇快速体验 <em class="not-italic text-ember-500">InfiPlot</em>。点击「<span class="inline-flex items-center gap-1 text-ember-500"><i class="fa-solid fa-gear text-[10px]"></i>设置</span>」还能填入你的名字,以及你自己的文本、绘图、识图模型和配音 Key——全部只存在本地浏览器,体验更稳定。`;
|
||||
},
|
||||
closeAriaLabel: "不再显示此提示",
|
||||
},
|
||||
|
||||
// About section
|
||||
about: {
|
||||
title: "InfiPlot",
|
||||
description: "是一款用 AI 实时生成内容的交互式剧情游戏 —— 图片、语音与剧情分支都在游玩过程中即时生成。",
|
||||
team: "团 队",
|
||||
teamText: "我们来自清华大学、兰州大学等高校,希望探索多模态模型在「直接生成图片、视频」这类 oneshot 能力之外,更多的可能性。本项目目前仍处于早期阶段,我们还在招募成员,如果你也感兴趣,欢迎联系我们,期待你的加入。",
|
||||
contact: "联 系 方 式",
|
||||
email: "邮箱",
|
||||
openSource: "开 源 地 址",
|
||||
betaUsers: "内 测 用 户 群",
|
||||
qqGroupLabel: "QQ群号:",
|
||||
qqGroupAlt: "InfiPlot 公测交流群 QQ 群二维码(群号 575404333)",
|
||||
legalNotice: (params: { analyticsOn?: boolean }) => {
|
||||
const base = `公测期间本产品可免费使用,但稳定性可能会随并发用户数量而有波动。<br />公测期间生成的内容不会在服务器上保存。如需留存,请在游玩结束后使用导出图集或分享剧情功能保存您的游玩体验。<br />AI 生成的内容不代表本团队立场。`;
|
||||
if (params.analyticsOn) {
|
||||
return `${base}<br />本站使用开源的 <a href="https://umami.is/" target="_blank" rel="noopener noreferrer">Umami</a> 进行隐私友好的匿名访问与交互统计:不使用 Cookie、不收集个人信息、不发送任何您输入的内容、不做跨站追踪。`;
|
||||
}
|
||||
return base;
|
||||
},
|
||||
privacyPolicy: "隐私政策",
|
||||
terms: "服务条款",
|
||||
copyright: "© 2026 InfiPlot. All rights reserved.",
|
||||
},
|
||||
|
||||
// Story import errors
|
||||
errors: {
|
||||
emptyFile: "这个剧情文件是空的。",
|
||||
fileTooLarge: "剧情文件太大,无法载入。",
|
||||
unpackFailed: "剧情文件解包失败。",
|
||||
parseFailed: "剧情文件解析失败。",
|
||||
cardNotFound: "找不到精选剧情:{cardName}",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Play Page (PlayCanvas.tsx & play/page.tsx) ==========
|
||||
play: {
|
||||
// Loading states
|
||||
loading: {
|
||||
firstFrame: "正 · 在 · 绘 · 制 · 第 · 一 · 幕",
|
||||
transitioning: "AI · 正 · 在 · 描 · 画 · 下 · 一 · 幕",
|
||||
visionThinking: "AI · 正 · 在 · 想 · 你 · 看 · 到 · 了 · 什 · 么",
|
||||
loadingFirst: "正 · 在 · 唤 · 起 · 第 · 一 · 幕",
|
||||
awakening: "载入中",
|
||||
},
|
||||
|
||||
// Freeform input
|
||||
freeform: {
|
||||
placeholder: "输入你想说的或想做的...",
|
||||
title: "自由输入",
|
||||
ariaLabel: "自由输入",
|
||||
},
|
||||
|
||||
// Choice disabled title
|
||||
choiceDisabled: "分享剧情未包含这条分支",
|
||||
|
||||
// Tooltips
|
||||
tooltips: {
|
||||
openSettings: "打开设置",
|
||||
openHistory: "剧情回溯",
|
||||
fullscreen: "全屏 (F)",
|
||||
enterFullscreen: "进入全屏",
|
||||
exportGallery: "导出本局为可交互图集链接(含配音;只会保留最近两次的可交互图集链接)",
|
||||
exportGalleryLabel: "导出可交互图集",
|
||||
shareStory: "导出本局为可继续游玩的剧情 .infiplot(含配音)",
|
||||
shareStoryLabel: "分享当前剧情",
|
||||
mute: "静音",
|
||||
unmute: "取消静音",
|
||||
closeNudge: "关闭提示",
|
||||
silenceNudge: "效果不满意/经常没声音?填入自己的 API Key 试试",
|
||||
back: "返回",
|
||||
},
|
||||
|
||||
// Image alt
|
||||
imageAlt: "Generated scene",
|
||||
|
||||
// Scene/beat counter
|
||||
counter: {
|
||||
scene: "第 · {n} · 幕",
|
||||
beat: "{n} · 拍",
|
||||
middle: "·",
|
||||
},
|
||||
|
||||
// Button labels
|
||||
buttons: {
|
||||
fullscreen: "F · 键 · 全 · 屏",
|
||||
exportGallery: "导 · 出 · 图 · 集",
|
||||
shareStory: "分 · 享 · 剧 · 情",
|
||||
muted: "静 · 音",
|
||||
sound: "有 · 声",
|
||||
},
|
||||
|
||||
// Error state
|
||||
error: {
|
||||
title: "出 · 了 · 点 · 状 · 况",
|
||||
back: "返 · 回",
|
||||
},
|
||||
|
||||
// Previous action
|
||||
previousStep: "上 · 一 · 步 ·",
|
||||
|
||||
// Settings footer note
|
||||
settingsFooter: "保存后配音 Key 会立即生效,用你自己的额度合成当前这一幕的配音。",
|
||||
|
||||
// Share file errors
|
||||
shareErrors: {
|
||||
notFound: "没有找到要载入的剧情文件。",
|
||||
invalid: "剧情分享文件没有可载入的剧情。",
|
||||
noImage: "剧情分享文件缺少第一幕图片。",
|
||||
noNextImage: "剧情分享文件缺少下一幕图片。",
|
||||
noMemory: "剧情分享文件缺少初始剧情记忆,无法载入。",
|
||||
packFailed: "剧情分享打包失败",
|
||||
},
|
||||
|
||||
// Export progress
|
||||
exportProgress: {
|
||||
preparingVoice: "正在准备配音",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Settings Modal (SettingsModal.tsx) ==========
|
||||
settings: {
|
||||
title: "设置",
|
||||
subtitle: "可选 · 这些设置仅保存在本地浏览器",
|
||||
|
||||
// Tabs
|
||||
tabs: {
|
||||
general: "通用",
|
||||
models: "模型",
|
||||
},
|
||||
|
||||
// General tab
|
||||
general: {
|
||||
playerName: "玩家名字",
|
||||
playerNamePlaceholder: "不填则使用「你」",
|
||||
playerNameHint: "NPC 会在对话中用这个名字称呼你。不填则默认以「你」称呼。",
|
||||
visionClick: "点击画面识别",
|
||||
visionOn: "开启",
|
||||
visionOff: "关闭",
|
||||
visionHint: "开启后,在选择节点点击画面会触发 AI 识图并生成新的剧情分支。",
|
||||
},
|
||||
|
||||
// Models tab
|
||||
models: {
|
||||
corsNotice: "请确保你的 API 端点支持浏览器跨域请求(CORS)。大多数主流提供商(OpenAI、Anthropic、Gemini、Runware 等)已默认支持。",
|
||||
textModel: "文本模型",
|
||||
imageModel: "绘图模型",
|
||||
visionModel: "识图模型",
|
||||
baseUrl: "BASE URL",
|
||||
apiKey: "API Key",
|
||||
model: "Model",
|
||||
provider: "Provider(可选)",
|
||||
providerHint: "留空时系统会根据 Base URL 自动推断协议。",
|
||||
providerAuto: "自动推断(推荐)",
|
||||
show: "显示",
|
||||
hide: "隐藏",
|
||||
},
|
||||
|
||||
// TTS section
|
||||
tts: {
|
||||
title: "配音模型",
|
||||
description: '填入你自己的 <span class="text-clay-800">小米 MiMo API Key</span>,配音将在浏览器本地合成,Key 只保存在本地、绝不经过服务器。MiMo TTS 目前<span class="text-clay-800">限时免费</span>,申请即可使用。',
|
||||
keyType: "Key 类型",
|
||||
payg: "按量付费 Pay-as-you-go",
|
||||
paygSub: "sk- 开头",
|
||||
tokenPlan: "套餐 Token Plan",
|
||||
tokenPlanSub: "tp- 开头",
|
||||
region: "区域节点",
|
||||
regionHint: "选择与你的套餐订阅地区一致的节点(通常也是延迟最低的那个)。",
|
||||
apiKeyPlaceholderPayg: "粘贴 sk- 开头的按量 Key",
|
||||
apiKeyPlaceholderToken: "粘贴 tp- 开头的套餐 Key",
|
||||
keyMismatchPayg: '此 Key 不是 sk- 开头,可能与所选「按量付费 Pay-as-you-go」类型不符,请确认是否填错。',
|
||||
keyMismatchToken: '此 Key 不是 tp- 开头,可能与所选「套餐 Token Plan」类型不符,请确认是否填错。',
|
||||
tutorialLink: "如何免费申请 Key?查看图文教程",
|
||||
},
|
||||
|
||||
// Actions
|
||||
actions: {
|
||||
save: "保存",
|
||||
clearAll: "全部清除",
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Auth Modal (AuthModal.tsx) ==========
|
||||
auth: {
|
||||
// Steps
|
||||
steps: {
|
||||
pick: "登录以继续",
|
||||
email: "邮箱登录",
|
||||
otp: "验证码",
|
||||
},
|
||||
|
||||
// Buttons
|
||||
googleLogin: "Google 登录",
|
||||
githubLogin: "GitHub 登录",
|
||||
emailLogin: "邮箱验证码登录",
|
||||
or: "或",
|
||||
|
||||
// Email input
|
||||
emailPlaceholder: "your@email.com",
|
||||
sendCode: "发送验证码",
|
||||
sending: "发送中...",
|
||||
|
||||
// OTP verification
|
||||
codeSent: "验证码已发送至 {email}",
|
||||
codePlaceholder: "6 位验证码",
|
||||
verify: "确认",
|
||||
verifying: "验证中...",
|
||||
resend: "重新发送",
|
||||
|
||||
// Navigation
|
||||
back: "返回",
|
||||
|
||||
// Close
|
||||
close: "关闭",
|
||||
|
||||
// Aria labels
|
||||
ariaLabel: "登录",
|
||||
},
|
||||
|
||||
// ========== Dialogue History Modal ==========
|
||||
history: {
|
||||
title: "剧 · 情 · 回 · 溯",
|
||||
close: "关闭",
|
||||
closeAriaLabel: "关闭剧情回溯",
|
||||
noHistory: "暂无历史。",
|
||||
scene: "第 {n} 幕",
|
||||
choice: "选择",
|
||||
action: "行动",
|
||||
ariaLabel: "剧情回溯",
|
||||
},
|
||||
|
||||
// ========== Custom Form (CustomForm.tsx) ==========
|
||||
customForm: {
|
||||
world: "World · 世界观",
|
||||
style: "Style · 画风",
|
||||
worldPlaceholder: "例:1990 年代末的中国南方县城。主角是高三转学生,在多雨的六月遇到一个总在天台读诗的同学。剧情慢热、含蓄、带点伤感⋯",
|
||||
stylePlaceholder: "例:水彩柔光,午后暖意,动漫视觉小说画风,传统对话面板⋯",
|
||||
status: {
|
||||
ready: "准 · 备 · 就 · 绪",
|
||||
needMore: "两 · 段 · 即 · 可 · 开 · 场",
|
||||
starting: "正在唤起第一帧…",
|
||||
},
|
||||
start: "开 始",
|
||||
},
|
||||
|
||||
// ========== Language Switcher ==========
|
||||
language: {
|
||||
title: "语言",
|
||||
current: "当前语言",
|
||||
select: "选择语言",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ZhCNTranslations = typeof zhCN;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Traditional Chinese (Hong Kong)
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const zhHK = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? '(測試期間,登入即可免費暢玩)' : '';
|
||||
return `輸入想法、配置風格,點擊「開始」即可遊玩${authHint};也可以從下方精選故事集挑一篇快速體驗 <em>InfiPlot</em>。點擊「設置」還能填入你的名字,以及你自己的文本、繪圖、識圖模型和配音 Key——全部只存在本地瀏覽器,體驗更穩定。`;
|
||||
},
|
||||
"closeAriaLabel": "不再顯示此提示"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type ZhHKTranslations = typeof zhHK;
|
||||
@@ -0,0 +1,89 @@
|
||||
// Traditional Chinese (Taiwan)
|
||||
// Auto-generated by scripts/translate-i18n.mjs
|
||||
|
||||
export const zhTW = {
|
||||
"layout": {
|
||||
"metadata": {
|
||||
"title": "InfiPlot",
|
||||
"description": "InfiPlot"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"examples": {
|
||||
"male": [],
|
||||
"female": [],
|
||||
"x": []
|
||||
},
|
||||
"options": {
|
||||
"gender": "",
|
||||
"artStyle": "",
|
||||
"plotStyle": "",
|
||||
"voice": "",
|
||||
"pacing": ""
|
||||
},
|
||||
"genders": {
|
||||
"male": "",
|
||||
"female": "",
|
||||
"x": ""
|
||||
},
|
||||
"artStyles": {},
|
||||
"plotStyles": {
|
||||
"straightforward": "",
|
||||
"twist": ""
|
||||
},
|
||||
"voiceOptions": {
|
||||
"off": "",
|
||||
"on": ""
|
||||
},
|
||||
"pacings": {
|
||||
"fast": "",
|
||||
"relaxed": ""
|
||||
},
|
||||
"stories": {},
|
||||
"ui": {
|
||||
"start": "",
|
||||
"loadStory": "",
|
||||
"settings": "",
|
||||
"searchPlaceholder": "",
|
||||
"noMatchingStyle": "",
|
||||
"close": "",
|
||||
"back": "",
|
||||
"save": "",
|
||||
"cancel": "",
|
||||
"saveAndSelect": ""
|
||||
},
|
||||
"styleModal": {},
|
||||
"hero": {
|
||||
"title": "",
|
||||
"placeholder": " ",
|
||||
"enterHint": ""
|
||||
},
|
||||
"hint": {
|
||||
"text": (params: { authEnabled?: boolean }) => {
|
||||
const authHint = params.authEnabled ? '(測試期間,登入即可免費暢玩)' : '';
|
||||
return `輸入想法、配置風格,點擊「開始」即可遊玩${authHint};也可以從下方精選故事集挑一篇快速體驗 <em>InfiPlot</em>。點擊「設置」還能填入你的名字,以及你自己的文本、繪圖、識圖模型和配音 Key——全部只存在本地瀏覽器,體驗更穩定。`;
|
||||
},
|
||||
"closeAriaLabel": "不再顯示此提示"
|
||||
},
|
||||
"about": {},
|
||||
"errors": {
|
||||
"emptyFile": "",
|
||||
"fileTooLarge": "",
|
||||
"unpackFailed": "",
|
||||
"parseFailed": "",
|
||||
"cardNotFound": ""
|
||||
}
|
||||
},
|
||||
"play": {},
|
||||
"settings": {},
|
||||
"auth": {},
|
||||
"history": {},
|
||||
"customForm": {},
|
||||
"language": {
|
||||
"title": "",
|
||||
"current": "",
|
||||
"select": ""
|
||||
}
|
||||
} as const;
|
||||
|
||||
export type ZhTWTranslations = typeof zhTW;
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { Locale } from "./config";
|
||||
import { DEFAULT_LOCALE, getInitialLocale } from "./config";
|
||||
import { getNestedValue, formatTranslation } from "./utils";
|
||||
|
||||
// Server-side translation cache
|
||||
const translationCache = new Map<Locale, Record<string, unknown>>();
|
||||
|
||||
// Get locale from request headers
|
||||
export function getLocaleFromHeaders(headers: Headers): Locale {
|
||||
// Check for custom locale header
|
||||
const customLocale = headers.get("x-locale");
|
||||
if (customLocale) {
|
||||
return customLocale as Locale;
|
||||
}
|
||||
|
||||
// Check Accept-Language header
|
||||
const acceptLanguage = headers.get("accept-language");
|
||||
if (acceptLanguage) {
|
||||
const browserLang = acceptLanguage.split(",")[0]?.split("-")[0];
|
||||
// Map common language codes to our locales
|
||||
const localeMap: Record<string, Locale> = {
|
||||
en: "en",
|
||||
zh: "zh-CN",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
de: "de",
|
||||
pt: "pt",
|
||||
ru: "ru",
|
||||
it: "it",
|
||||
vi: "vi",
|
||||
th: "th",
|
||||
id: "id",
|
||||
tr: "tr",
|
||||
pl: "pl",
|
||||
nl: "nl",
|
||||
uk: "uk",
|
||||
hi: "hi",
|
||||
cs: "cs",
|
||||
};
|
||||
|
||||
const browserLangBase = acceptLanguage.split(",")[0]?.split("-")[0];
|
||||
if (browserLangBase) {
|
||||
const matched = localeMap[browserLangBase];
|
||||
if (matched) return matched;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
// Load translations for server-side
|
||||
export async function loadTranslations(locale: Locale): Promise<Record<string, unknown>> {
|
||||
// Check cache first
|
||||
if (translationCache.has(locale)) {
|
||||
return translationCache.get(locale)!;
|
||||
}
|
||||
|
||||
try {
|
||||
// Dynamic import based on locale
|
||||
let translations;
|
||||
switch (locale) {
|
||||
case "zh-CN":
|
||||
translations = (await import("./locales/zh-CN")).zhCN;
|
||||
break;
|
||||
case "en":
|
||||
translations = (await import("./locales/en")).en;
|
||||
break;
|
||||
case "zh-TW":
|
||||
translations = (await import("./locales/zh-TW")).zhTW;
|
||||
break;
|
||||
case "zh-HK":
|
||||
translations = (await import("./locales/zh-HK")).zhHK;
|
||||
break;
|
||||
case "ja":
|
||||
translations = (await import("./locales/ja")).ja;
|
||||
break;
|
||||
case "ko":
|
||||
translations = (await import("./locales/ko")).ko;
|
||||
break;
|
||||
case "es":
|
||||
translations = (await import("./locales/es")).es;
|
||||
break;
|
||||
case "fr":
|
||||
translations = (await import("./locales/fr")).fr;
|
||||
break;
|
||||
case "de":
|
||||
translations = (await import("./locales/de")).de;
|
||||
break;
|
||||
case "pt-BR":
|
||||
translations = (await import("./locales/pt-BR")).ptBR;
|
||||
break;
|
||||
case "pt":
|
||||
translations = (await import("./locales/pt")).pt;
|
||||
break;
|
||||
case "ru":
|
||||
translations = (await import("./locales/ru")).ru;
|
||||
break;
|
||||
case "it":
|
||||
translations = (await import("./locales/it")).it;
|
||||
break;
|
||||
case "vi":
|
||||
translations = (await import("./locales/vi")).vi;
|
||||
break;
|
||||
case "th":
|
||||
translations = (await import("./locales/th")).th;
|
||||
break;
|
||||
case "id":
|
||||
translations = (await import("./locales/id")).id;
|
||||
break;
|
||||
case "tr":
|
||||
translations = (await import("./locales/tr")).tr;
|
||||
break;
|
||||
case "pl":
|
||||
translations = (await import("./locales/pl")).pl;
|
||||
break;
|
||||
case "nl":
|
||||
translations = (await import("./locales/nl")).nl;
|
||||
break;
|
||||
case "uk":
|
||||
translations = (await import("./locales/uk")).uk;
|
||||
break;
|
||||
case "hi":
|
||||
translations = (await import("./locales/hi")).hi;
|
||||
break;
|
||||
case "cs":
|
||||
translations = (await import("./locales/cs")).cs;
|
||||
break;
|
||||
default:
|
||||
console.warn(`Translations for ${locale} not found, using English fallback`);
|
||||
translations = (await import("./locales/en")).en;
|
||||
break;
|
||||
}
|
||||
|
||||
translationCache.set(locale, translations as Record<string, unknown>);
|
||||
return translations as Record<string, unknown>;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load translations for ${locale}:`, error);
|
||||
// Fallback to default locale
|
||||
const fallback = await import("./locales/zh-CN");
|
||||
return fallback.zhCN as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side translation function
|
||||
export async function getTranslations(locale: Locale): Promise<Record<string, unknown>> {
|
||||
return loadTranslations(locale);
|
||||
}
|
||||
|
||||
// Create a translation function for server components
|
||||
export function createTranslator(translations: Record<string, unknown>) {
|
||||
return function t(key: string, params: Record<string, string | number | boolean> = {}): string {
|
||||
const value = getNestedValue(translations, key);
|
||||
|
||||
if (value === undefined) {
|
||||
console.warn(`Translation missing for key: ${key}`);
|
||||
return key;
|
||||
}
|
||||
|
||||
if (typeof value === "function") {
|
||||
return (value as (params: Record<string, string | number | boolean>) => string)(params);
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return formatTranslation(value, params);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
}
|
||||
|
||||
// Get initial locale for server components
|
||||
export function getServerLocale(): Locale {
|
||||
return DEFAULT_LOCALE; // Will be overridden by middleware in production
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Locale } from "./config";
|
||||
|
||||
// Translation value type - can be a string or a function that takes parameters
|
||||
export type TranslationValue = string | ((params: Record<string, string | number>) => string);
|
||||
|
||||
// Translation structure - nested objects with translation values at leaves
|
||||
export type TranslationStructure = Record<string, unknown>;
|
||||
|
||||
// Flatten a nested object to dot-notation keys
|
||||
export type Flatten<T> = T extends object
|
||||
? {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any
|
||||
? T[K]
|
||||
: T[K] extends object
|
||||
? Flatten<T[K]>
|
||||
: T[K];
|
||||
}[keyof T]
|
||||
: T;
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Locale } from "./config";
|
||||
|
||||
/**
|
||||
* Get a nested value from an object using a dot-notation path
|
||||
* @example getNestedValue({ a: { b: "c" } }, "a.b") // "c"
|
||||
*/
|
||||
export function getNestedValue<T>(obj: T, path: string): unknown {
|
||||
return path.split(".").reduce<unknown>((current, key) => {
|
||||
if (current && typeof current === "object" && key in current) {
|
||||
return (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return undefined;
|
||||
}, obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a translation string with parameters
|
||||
* Supports both {{key}} syntax and simple function-based interpolation
|
||||
*/
|
||||
export function formatTranslation(
|
||||
template: string,
|
||||
params: Record<string, string | number | boolean>,
|
||||
): string {
|
||||
if (Object.keys(params).length === 0) return template;
|
||||
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
||||
return params[key]?.toString() ?? `{{${key}}}`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep merge two objects
|
||||
*/
|
||||
export function deepMerge<T extends Record<string, unknown>>(
|
||||
target: T,
|
||||
source: Partial<T>,
|
||||
): T {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key in source) {
|
||||
if (source[key] && typeof source[key] === "object" && !Array.isArray(source[key])) {
|
||||
if (result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
|
||||
result[key] = deepMerge(
|
||||
result[key] as Record<string, unknown>,
|
||||
source[key] as Record<string, unknown>,
|
||||
) as T[Extract<keyof T, string>];
|
||||
} else {
|
||||
result[key] = source[key] as T[Extract<keyof T, string>];
|
||||
}
|
||||
} else {
|
||||
result[key] = source[key] as T[Extract<keyof T, string>];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate locale string
|
||||
*/
|
||||
export function isValidLocale(locale: string): locale is Locale {
|
||||
const validLocales: Locale[] = [
|
||||
"en",
|
||||
"zh-CN",
|
||||
"zh-TW",
|
||||
"zh-HK",
|
||||
"ja",
|
||||
"ko",
|
||||
"es",
|
||||
"fr",
|
||||
"de",
|
||||
"pt-BR",
|
||||
"pt",
|
||||
"ru",
|
||||
"it",
|
||||
"vi",
|
||||
"th",
|
||||
"id",
|
||||
"tr",
|
||||
"pl",
|
||||
"nl",
|
||||
"uk",
|
||||
"hi",
|
||||
"cs",
|
||||
];
|
||||
return validLocales.includes(locale as Locale);
|
||||
}
|
||||
Reference in New Issue
Block a user