code:
/**
 * Very scuffed request queue. OpenAI's GPT-4 keys have a very strict rate limit
 * of 40000 generated tokens per minute. We don't actually know how many tokens
 * a given key has generated, so our queue will simply retry requests that fail
 * with a non-billing related 429 over and over again until they succeed.
 *
 * Dequeueing can operate in one of two modes:
 * - 'fair': requests are dequeued in the order they were enqueued.
 * - 'random': requests are dequeued randomly, not really a queue at all.
 *
 * When a request to a proxied endpoint is received, we create a closure around
 * the call to http-proxy-middleware and attach it to the request. This allows
 * us to pause the request until we have a key available. Further, if the
 * proxied request encounters a retryable error, we can simply put the request
 * back in the queue and it will be retried later using the same closure.
 */

import type { Handler, Request } from "express";
import { config, DequeueMode } from "../config";
import { keyPool } from "../key-management";
import { logger } from "../logger";
import { AGNAI_DOT_CHAT_IP } from "./rate-limit";

const queue: Request[] = [];
const log = logger.child({ module: "request-queue" });

let dequeueMode: DequeueMode = "fair";

/** Maximum number of queue slots for Agnai.chat requests. */
const AGNAI_CONCURRENCY_LIMIT = 15;
/** Maximum number of queue slots for individual users. */
const USER_CONCURRENCY_LIMIT = 1;

const sameIpPredicate = (incoming: Request) => (queued: Request) =>
  queued.ip === incoming.ip;
const sameUserPredicate = (incoming: Request) => (queued: Request) => {
  const incomingUser = incoming.user ?? { token: incoming.ip };
  const queuedUser = queued.user ?? { token: queued.ip };
  return queuedUser.token === incomingUser.token;
};

export function enqueue(req: Request) {
  let enqueuedRequestCount = 0;
  let isGuest = req.user?.token === undefined;

  if (isGuest) {
    enqueuedRequestCount = queue.filter(sameIpPredicate(req)).length;
  } else {
    enqueuedRequestCount = queue.filter(sameUserPredicate(req)).length;
  }

  // All Agnai.chat requests come from the same IP, so we allow them to have
  // more spots in the queue. Can't make it unlimited because people will
  // intentionally abuse it.
  // Authenticated users always get a single spot in the queue.
  const maxConcurrentQueuedRequests =
    isGuest && req.ip === AGNAI_DOT_CHAT_IP
      ? AGNAI_CONCURRENCY_LIMIT
      : USER_CONCURRENCY_LIMIT;
  if (enqueuedRequestCount >= maxConcurrentQueuedRequests) {
    if (req.ip === AGNAI_DOT_CHAT_IP) {
      // Re-enqueued requests are not counted towards the limit since they
      // already made it through the queue once.
      if (req.retryCount === 0) {
        throw new Error("Too many agnai.chat requests are already queued");
      }
    } else {
      throw new Error("Your IP or token already has a request in the queue");
    }
  }

  queue.push(req);
  req.queueOutTime = 0;

  // shitty hack to remove hpm's event listeners on retried requests
  removeProxyMiddlewareEventListeners(req);

  // If the request opted into streaming, we need to register a heartbeat
  // handler to keep the connection alive while it waits in the queue. We
  // deregister the handler when the request is dequeued.
  if (req.body.stream) {
    const res = req.res!;
    if (!res.headersSent) {
      initStreaming(req);
    }
    req.heartbeatInterval = setInterval(() => {
      if (process.env.NODE_ENV === "production") {
        req.res!.write(": queue heartbeat\n\n");
      } else {
        req.log.info(`Sending heartbeat to request in queue.`);
        const avgWait = Math.round(getEstimatedWaitTime() / 1000);
        const currentDuration = Math.round((Date.now() - req.startTime) / 1000);
        const debugMsg = `queue length: ${queue.length}; elapsed time: ${currentDuration}s; avg wait: ${avgWait}s`;
        req.res!.write(buildFakeSseMessage("heartbeat", debugMsg));
      }
    }, 10000);
  }

  // Register a handler to remove the request from the queue if the connection
  // is aborted or closed before it is dequeued.
  const removeFromQueue = () => {
    req.log.info(`Removing aborted request from queue.`);
    const index = queue.indexOf(req);
    if (index !== -1) {
      queue.splice(index, 1);
    }
    if (req.heartbeatInterval) {
      clearInterval(req.heartbeatInterval);
    }
  };
  req.onAborted = removeFromQueue;
  req.res!.once("close", removeFromQueue);

  if (req.retryCount ?? 0 > 0) {
    req.log.info({ retries: req.retryCount }, `Enqueued request for retry.`);
  } else {
    req.log.info(`Enqueued new request.`);
  }
}

export function dequeue(model: string): Request | undefined {
  // TODO: This should be set by some middleware that checks the request body.
  const modelQueue =
    model === "gpt-4"
      ? queue.filter((req) => req.body.model?.startsWith("gpt-4"))
      : queue.filter((req) => !req.body.model?.startsWith("gpt-4"));

  if (modelQueue.length === 0) {
    return undefined;
  }

  let req: Request;

  if (dequeueMode === "fair") {
    // Dequeue the request that has been waiting the longest
    req = modelQueue.reduce((prev, curr) =>
      prev.startTime < curr.startTime ? prev : curr
    );
  } else {
    // Dequeue a random request
    const index = Math.floor(Math.random() * modelQueue.length);
    req = modelQueue[index];
  }
  queue.splice(queue.indexOf(req), 1);

  if (req.onAborted) {
    req.res!.off("close", req.onAborted);
    req.onAborted = undefined;
  }

  if (req.heartbeatInterval) {
    clearInterval(req.heartbeatInterval);
  }

  // Track the time leaving the queue now, but don't add it to the wait times
  // yet because we don't know if the request will succeed or fail. We track
  // the time now and not after the request succeeds because we don't want to
  // include the model processing time.
  req.queueOutTime = Date.now();
  return req;
}

/**
 * Naive way to keep the queue moving by continuously dequeuing requests. Not
 * ideal because it limits throughput but we probably won't have enough traffic
 * or keys for this to be a problem.  If it does we can dequeue multiple
 * per tick.
 **/
function processQueue() {
  // This isn't completely correct, because a key can service multiple models.
  // Currently if a key is locked out on one model it will also stop servicing
  // the others, because we only track one rate limit per key.
  const gpt4Lockout = keyPool.getLockoutPeriod("gpt-4");
  const turboLockout = keyPool.getLockoutPeriod("gpt-3.5-turbo");

  const reqs: (Request | undefined)[] = [];
  if (gpt4Lockout === 0) {
    reqs.push(dequeue("gpt-4"));
  }
  if (turboLockout === 0) {
    reqs.push(dequeue("gpt-3.5-turbo"));
  }

  reqs.filter(Boolean).forEach((req) => {
    if (req?.proceed) {
      req.log.info({ retries: req.retryCount }, `Dequeuing request.`);
      req.proceed();
    }
  });
  setTimeout(processQueue, 50);
}

/**
 * Kill stalled requests after 5 minutes, and remove tracked wait times after 2
 * minutes.
 **/
function cleanQueue() {
  const now = Date.now();
  const oldRequests = queue.filter(
    (req) => now - (req.startTime ?? now) > 5 * 60 * 1000
  );
  oldRequests.forEach((req) => {
    req.log.info(`Removing request from queue after 5 minutes.`);
    killQueuedRequest(req);
  });

  const index = waitTimes.findIndex(
    (waitTime) => now - waitTime.end > 300 * 1000
  );
  const removed = waitTimes.splice(0, index + 1);
  log.debug(
    { stalledRequests: oldRequests.length, prunedWaitTimes: removed.length },
    `Cleaning up request queue.`
  );
  setTimeout(cleanQueue, 20 * 1000);
}

export function start() {
  processQueue();
  cleanQueue();
  log.info(`Started request queue.`);
}

let waitTimes: { start: number; end: number }[] = [];

/** Adds a successful request to the list of wait times. */
export function trackWaitTime(req: Request) {
  waitTimes.push({
    start: req.startTime!,
    end: req.queueOutTime ?? Date.now(),
  });
}

/** Returns average wait time in milliseconds. */
export function getEstimatedWaitTime() {
  const now = Date.now();
  const recentWaits = waitTimes.filter((wt) => now - wt.end < 300 * 1000);
  if (recentWaits.length === 0) {
    return 0;
  }

  return (
    recentWaits.reduce((sum, wt) => sum + wt.end - wt.start, 0) /
    recentWaits.length
  );
}

export function getQueueLength() {
  return queue.length;
}

export function createQueueMiddleware(proxyMiddleware: Handler): Handler {
  return (req, res, next) => {
    if (config.queueMode === "none") {
      return proxyMiddleware(req, res, next);
    }

    req.proceed = () => {
      proxyMiddleware(req, res, next);
    };

    try {
      enqueue(req);
    } catch (err: any) {
      req.res!.status(429).json({
        type: "proxy_error",
        message: err.message,
        stack: err.stack,
        proxy_note: `Only one request per IP can be queued at a time. If you don't have another request queued, your IP may be in use by another user.`,
      });
    }
  };
}

function killQueuedRequest(req: Request) {
  if (!req.res || req.res.writableEnded) {
    req.log.warn(`Attempted to terminate request that has already ended.`);
    return;
  }
  const res = req.res;
  try {
    const message = `Your request has been terminated by the proxy because it has been in the queue for more than 5 minutes. The queue is currently ${queue.length} requests long.`;
    if (res.headersSent) {
      const fakeErrorEvent = buildFakeSseMessage("proxy queue error", message);
      res.write(fakeErrorEvent);
      res.end();
    } else {
      res.status(500).json({ error: message });
    }
  } catch (e) {
    req.log.error(e, `Error killing stalled request.`);
  }
}

function initStreaming(req: Request) {
  req.log.info(`Initiating streaming for new queued request.`);
  const res = req.res!;
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("X-Accel-Buffering", "no"); // nginx-specific fix
  res.flushHeaders();
  res.write("\n");
  res.write(": joining queue\n\n");
}

export function buildFakeSseMessage(type: string, string: string) {
  const fakeEvent = {
    id: "chatcmpl-" + type,
    object: "chat.completion.chunk",
    created: Date.now(),
    model: "",
    choices: [
      {
        delta: { content: `\`\`\`\n[${type}: ${string}]\n\`\`\`\n` },
        index: 0,
        finish_reason: type,
      },
    ],
  };
  return `data: ${JSON.stringify(fakeEvent)}\n\n`;
}

/**
 * http-proxy-middleware attaches a bunch of event listeners to the req and
 * res objects which causes problems with our approach to re-enqueuing failed
 * proxied requests. This function removes those event listeners.
 * We don't have references to the original event listeners, so we have to
 * look through the list and remove HPM's listeners by looking for particular
 * strings in the listener functions. This is an astoundingly shitty way to do
 * this, but it's the best I can come up with.
 */
function removeProxyMiddlewareEventListeners(req: Request) {
  // node_modules/http-proxy-middleware/dist/plugins/default/debug-proxy-errors-plugin.js:29
  // res.listeners('close')
  const RES_ONCLOSE = `Destroying proxyRes in proxyRes close event`;
  // node_modules/http-proxy-middleware/dist/plugins/default/debug-proxy-errors-plugin.js:19
  // res.listeners('error')
  const RES_ONERROR = `Socket error in proxyReq event`;
  // node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js:146
  // req.listeners('aborted')
  const REQ_ONABORTED = `proxyReq.abort()`;
  // node_modules/http-proxy/lib/http-proxy/passes/web-incoming.js:156
  // req.listeners('error')
  const REQ_ONERROR = `if (req.socket.destroyed`;

  const res = req.res!;

  const resOnClose = res
    .listeners("close")
    .find((listener) => listener.toString().includes(RES_ONCLOSE));
  if (resOnClose) {
    res.removeListener("close", resOnClose as any);
  }

  const resOnError = res
    .listeners("error")
    .find((listener) => listener.toString().includes(RES_ONERROR));
  if (resOnError) {
    res.removeListener("error", resOnError as any);
  }

  const reqOnAborted = req
    .listeners("aborted")
    .find((listener) => listener.toString().includes(REQ_ONABORTED));
  if (reqOnAborted) {
    req.removeListener("aborted", reqOnAborted as any);
  }

  const reqOnError = req
    .listeners("error")
    .find((listener) => listener.toString().includes(REQ_ONERROR));
  if (reqOnError) {
    req.removeListener("error", reqOnError as any);
  }
}


code:
import { Request, Response, NextFunction } from "express";
import { config } from "../config";

export const AGNAI_DOT_CHAT_IP = "157.230.249.32";
const RATE_LIMIT_ENABLED = Boolean(config.modelRateLimit);
const RATE_LIMIT = Math.max(1, config.modelRateLimit);
const ONE_MINUTE_MS = 60 * 1000;

const lastAttempts = new Map<string, number[]>();

const expireOldAttempts = (now: number) => (attempt: number) =>
  attempt > now - ONE_MINUTE_MS;

const getTryAgainInMs = (ip: string) => {
  const now = Date.now();
  const attempts = lastAttempts.get(ip) || [];
  const validAttempts = attempts.filter(expireOldAttempts(now));

  if (validAttempts.length >= RATE_LIMIT) {
    return validAttempts[0] - now + ONE_MINUTE_MS;
  } else {
    lastAttempts.set(ip, [...validAttempts, now]);
    return 0;
  }
};

const getStatus = (ip: string) => {
  const now = Date.now();
  const attempts = lastAttempts.get(ip) || [];
  const validAttempts = attempts.filter(expireOldAttempts(now));
  return {
    remaining: Math.max(0, RATE_LIMIT - validAttempts.length),
    reset: validAttempts.length > 0 ? validAttempts[0] + ONE_MINUTE_MS : now,
  };
};

/** Prunes attempts and IPs that are no longer relevant after one minutes. */
const clearOldAttempts = () => {
  const now = Date.now();
  for (const [ip, attempts] of lastAttempts.entries()) {
    const validAttempts = attempts.filter(expireOldAttempts(now));
    if (validAttempts.length === 0) {
      lastAttempts.delete(ip);
    } else {
      lastAttempts.set(ip, validAttempts);
    }
  }
};
setInterval(clearOldAttempts, 10 * 1000);

export const getUniqueIps = () => {
  return lastAttempts.size;
};

export const ipLimiter = (req: Request, res: Response, next: NextFunction) => {
  if (!RATE_LIMIT_ENABLED) {
    next();
    return;
  }

  // Exempt Agnai.chat from rate limiting since it's shared between a lot of
  // users. Dunno how to prevent this from being abused without some sort of
  // identifier sent from Agnaistic to identify specific users.
  if (req.ip === AGNAI_DOT_CHAT_IP) {
    next();
    return;
  }

  // If user is authenticated, key rate limiting by their token. Otherwise, key
  // rate limiting by their IP address. Mitigates key sharing.
  const rateLimitKey = req.user?.token || req.ip;

  const { remaining, reset } = getStatus(rateLimitKey);
  res.set("X-RateLimit-Limit", config.modelRateLimit.toString());
  res.set("X-RateLimit-Remaining", remaining.toString());
  res.set("X-RateLimit-Reset", reset.toString());

  const tryAgainInMs = getTryAgainInMs(rateLimitKey);
  if (tryAgainInMs > 0) {
    res.set("Retry-After", tryAgainInMs.toString());
    res.status(429).json({
      error: {
        type: "proxy_rate_limited",
        message: `This proxy is rate limited to ${
          config.modelRateLimit
        } model requests per minute. Please try again in ${Math.ceil(
          tryAgainInMs / 1000
        )} seconds.`,
      },
    });
  } else {
    next();
  }
};

repo_name: Nika/pronyan

license: unknown


code:
/* Accepts incoming requests at either the /kobold or /openai routes and then
routes them to the appropriate handler to be forwarded to the OpenAI API.
Incoming OpenAI requests are more or less 1:1 with the OpenAI API, but only a
subset of the API is supported. Kobold requests must be transformed into
equivalent OpenAI requests. */

import * as express from "express";
import { gatekeeper } from "./auth/gatekeeper";
import { kobold } from "./kobold";
import { openai } from "./openai";

const router = express.Router();

router.use(gatekeeper);
router.use("/kobold", kobold);
router.use("/openai", openai);

// Each client handles the endpoints input by the user in slightly different
// ways, eg TavernAI ignores everything after the hostname in Kobold mode
function rewriteTavernRequests(
  req: express.Request,
  _res: express.Response,
  next: express.NextFunction
) {
  // Requests coming into /api/v1 are actually requests to /proxy/kobold/api/v1
  if (req.path.startsWith("/api/v1")) {
    req.url = req.url.replace("/api/v1", "/proxy/kobold/api/v1");
  }
  next();
}

export { rewriteTavernRequests };
export { router as proxyRouter };


code:
import { assertConfigIsValid, config } from "./config";
import "source-map-support/register";
import express from "express";
import cors from "cors";
import pinoHttp from "pino-http";
import childProcess from "child_process";
import { logger } from "./logger";
import { keyPool } from "./key-management";
import { adminRouter } from "./admin/routes";
import { proxyRouter, rewriteTavernRequests } from "./proxy/routes";
import { handleInfoPage } from "./info-page";
import { logQueue } from "./prompt-logging";
import { start as startRequestQueue } from "./proxy/queue";
import { init as initUserStore } from "./proxy/auth/user-store";
import { checkOrigin } from "./proxy/check-origin";

const PORT = config.port;

const app = express();
// middleware
app.use("/", rewriteTavernRequests);
app.use(
  pinoHttp({
    quietReqLogger: true,
    logger,
    autoLogging: {
      ignore: (req) => {
        const ignored = ["/proxy/kobold/api/v1/model", "/health"];
        return ignored.includes(req.url as string);
      },
    },
    redact: {
      paths: [
        "req.headers.cookie",
        'res.headers["set-cookie"]',
        "req.headers.authorization",
        'req.headers["x-forwarded-for"]',
        'req.headers["x-real-ip"]',
        'req.headers["true-client-ip"]',
        'req.headers["cf-connecting-ip"]',
      ],
      censor: "********",
    },
  })
);

app.get("/health", (_req, res) => res.sendStatus(200));
app.use((req, _res, next) => {
  req.startTime = Date.now();
  req.retryCount = 0;
  next();
});
app.use(cors());
app.use(
  express.json({ limit: "10mb" }),
  express.urlencoded({ extended: true, limit: "10mb" })
);

// TODO: Detect (or support manual configuration of) whether the app is behind
// a load balancer/reverse proxy, which is necessary to determine request IP
// addresses correctly.
app.set("trust proxy", true);

// routes
app.use(checkOrigin);
app.get("/", handleInfoPage);
app.use("/admin", adminRouter);
app.use("/proxy", proxyRouter);

// 500 and 404
app.use((err: any, _req: unknown, res: express.Response, _next: unknown) => {
  if (err.status) {
    res.status(err.status).json({ error: err.message });
  } else {
    logger.error(err);
    res.status(500).json({
      error: {
        type: "proxy_error",
        message: err.message,
        stack: err.stack,
        proxy_note: `Reverse proxy encountered an internal server error.`,
      },
    });
  }
});
app.use((_req: unknown, res: express.Response) => {
  res.status(404).json({ error: "Not found" });
});

async function start() {
  logger.info("Server starting up...");
  await setBuildInfo();

  logger.info("Checking configs and external dependencies...");
  await assertConfigIsValid();

  keyPool.init();

  if (config.gatekeeper === "user_token") {
    await initUserStore();
  }

  if (config.promptLogging) {
    logger.info("Starting prompt logging...");
    logQueue.start();
  }

  if (config.queueMode !== "none") {
    logger.info("Starting request queue...");
    startRequestQueue();
  }

  app.listen(PORT, async () => {
    logger.info({ port: PORT }, "Now listening for connections.");
    registerUncaughtExceptionHandler();
  });

  logger.info(
    { build: process.env.BUILD_INFO, nodeEnv: process.env.NODE_ENV },
    "Startup complete."
  );
}

function registerUncaughtExceptionHandler() {
  process.on("uncaughtException", (err: any) => {
    logger.error(
      { err, stack: err?.stack },
      "UNCAUGHT EXCEPTION. Please report this error trace."
    );
  });
  process.on("unhandledRejection", (err: any) => {
    logger.error(
      { err, stack: err?.stack },
      "UNCAUGHT PROMISE REJECTION. Please report this error trace."
    );
  });
}

/**
 * Attepts to collect information about the current build from either the
 * environment or the git repo used to build the image (only works if not
 * .dockerignore'd). If you're running a sekrit club fork, you can no-op this
 * function and set the BUILD_INFO env var manually, though I would prefer you
 * didn't set it to something misleading.
 */
async function setBuildInfo() {
  // Render .dockerignore's the .git directory but provides info in the env
  if (process.env.RENDER) {
    const sha = process.env.RENDER_GIT_COMMIT?.slice(0, 7) || "unknown SHA";
    const branch = process.env.RENDER_GIT_BRANCH || "unknown branch";
    const repo = process.env.RENDER_GIT_REPO_SLUG || "unknown repo";
    const buildInfo = `${sha} (${branch}@${repo})`;
    process.env.BUILD_INFO = buildInfo;
    logger.info({ build: buildInfo }, "Got build info from Render config.");
    return;
  }

  try {
    // Ignore git's complaints about dubious directory ownership on Huggingface
    // (which evidently runs dockerized Spaces on Windows with weird NTFS perms)
    if (process.env.SPACE_ID) {
      childProcess.execSync("git config --global --add safe.directory /app");
    }

    const promisifyExec = (cmd: string) =>
      new Promise((resolve, reject) => {
        childProcess.exec(cmd, (err, stdout) =>
          err ? reject(err) : resolve(stdout)
        );
      });

    const promises = [
      promisifyExec("git rev-parse --short HEAD"),
      promisifyExec("git rev-parse --abbrev-ref HEAD"),
      promisifyExec("git config --get remote.origin.url"),
      promisifyExec("git status --porcelain"),
    ].map((p) => p.then((result: any) => result.toString().trim()));

    let [sha, branch, remote, status] = await Promise.all(promises);

    remote = remote.match(/.*[\/:]([\w-]+)\/([\w\-\.]+?)(?:\.git)?$/) || [];
    const repo = remote.slice(-2).join("/");
    status = status
      // ignore Dockerfile changes since that's how the user deploys the app
      .split("\n")
      .filter((line: string) => !line.endsWith("Dockerfile") && line);

    const changes = status.length > 0;

    const build = `${sha}${changes ? " (modified)" : ""} (${branch}@${repo})`;
    process.env.BUILD_INFO = build;
    logger.info({ build, status, changes }, "Got build info from Git.");
  } catch (error: any) {
    logger.error(
      {
        error,
        stdout: error.stdout.toString(),
        stderr: error.stderr.toString(),
      },
      "Failed to get commit SHA.",
      error
    );
    process.env.BUILD_INFO = "unknown";
  }
}

start();


code:
import { Express } from "express-serve-static-core";
import { Key } from "../key-management/key-pool";
import { User } from "../proxy/auth/user-store";

declare global {
  namespace Express {
    interface Request {
      key?: Key;
      api: "kobold" | "openai" | "anthropic";
      user?: User;
      isStreaming?: boolean;
      startTime: number;
      retryCount: number;
      queueOutTime?: number;
      onAborted?: () => void;
      proceed: () => void;
      heartbeatInterval?: NodeJS.Timeout;
    }
  }
}


code:
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain

repo_name: Matti/Koopa

path: .gitattributes

license: mit


code:
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
[Xx]64/
[Xx]86/
[Bb]uild/
bld/
[Bb]in/
[Oo]bj/

# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# DNX
project.lock.json
artifacts/

*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml

# TODO: Un-comment the next line if you do not want to checkin
# your web deploy settings because they may include unencrypted
# passwords
#*.pubxml
*.publishproj

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directory
AppPackages/
BundleArtifacts/

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

# Others
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
*.mdf
*.ldf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# LightSwitch generated files
GeneratedArtifacts/
ModelManifest.xml

# Paket dependency manager
.paket/paket.exe

# FAKE - F# Make
.fake/

path: .gitignore


code:
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pokémon Name Translator", "Pokémon Name Translator\Pokémon Name Translator.csproj", "{0E339E73-189A-475F-AD14-3915A82D6494}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{0E339E73-189A-475F-AD14-3915A82D6494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{0E339E73-189A-475F-AD14-3915A82D6494}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{0E339E73-189A-475F-AD14-3915A82D6494}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{0E339E73-189A-475F-AD14-3915A82D6494}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
EndGlobal

path: Pokémon Name Translator.sln


code:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" />
    </startup>
</configuration>

path: Pokémon Name Translator/App.config


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Pokémon_Name_Translator
{
    public static class Constants
    {
        public static string FILE_MOVE_NAMES = "Templates\\move_names.csv";
        public static string FILE_MOVES = "Templates\\moves.csv";
        public static string FILE_POKEMON_SPECIES_NAMES = "Templates\\pokemon_species_names.csv";
        public static string FILE_TYPE_NAMES = "Templates\\type_names.csv";

        public static Dictionary<int, string> TYPE_IMAGES()
        {
            return new Dictionary<int, string>()
            {
                {1, "http://i.imgur.com/39Whbtl.png" },   //Normal
                {2, "http://i.imgur.com/NRUneyw.png" },   //Fighting
                {3, "http://i.imgur.com/XUST30A.png" },   //Flying
                {4, "http://i.imgur.com/c7Guzjw.png" },   //Poison
                {5, "http://i.imgur.com/Coiqv54.png" },   //Ground
                {6, "http://i.imgur.com/lghZX1v.png" },   //Rock
                {7, "http://i.imgur.com/JGMm76x.png" },   //Bug
                {8, "http://i.imgur.com/T04FsAM.png" },   //Ghost
                {9, "http://i.imgur.com/jqv3XfC.png" },   //Steel
                {10, "http://i.imgur.com/N4Xomd6.png" },  //Fire
                {11, "http://i.imgur.com/pB5ffQv.png" },  //Water
                {12, "http://i.imgur.com/WdUbMjZ.png" },  //Grass
                {13, "http://i.imgur.com/oQldUb6.png" },  //Electric
                {14, "http://i.imgur.com/VW8O6g6.png" },  //Psychic
                {15, "http://i.imgur.com/mCuR0xO.png" },  //Ice
                {16, "http://i.imgur.com/ayE2j8i.png" },  //Dragon
                {17, "http://i.imgur.com/peNoKWm.png" },  //Dark
                {18, "http://i.imgur.com/oQldUb6.png" },  //Fairy
                {10001, "" },  //Unknown
                {10002, "" },  //Shadow
            };
        }

        public static string FILE_POST = "Post.txt";
        public static string FILE_PRE = "Pre.txt";
        public static string FILE_REPEAT = "Repeat.txt";
        public static string FILE_SEPARATOR = "Separator.txt";

        public static string REPLACE_NAME = "%NAME%";
        public static string REPLACE_INPUT = "%INPUT%";
        public static string REPLACE_OUTPUT = "%OUTPUT%";
    }
}

path: Pokémon Name Translator/Constants.cs


code:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Pokémon_Name_Translator
{
    public static class Helper
    {
        public static int GetLanguageOffset(Language.Languages inputLanguage, int[] supportedLanguages)
        {
            for (int offset = 0; offset < supportedLanguages.Length; offset++)
            {
                if (supportedLanguages[offset] == (int)inputLanguage)
                {
                    return offset;
                }
            }
            throw new Exception("Language not supported");
        }

        //public static string ToTitle(string input)
        //{
        //    return new CultureInfo("en-US").TextInfo.ToTitleCase(input);
        //}
    }
}

path: Pokémon Name Translator/Helper.cs


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Pokémon_Name_Translator
{
    public interface IBaseObject
    {
        int Id { get; set; }
        string InputName { get; set; }
        string Name { get; set; }
        string IconPlusName { get; }
    }
}

path: Pokémon Name Translator/IBaseObject.cs


code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Pokémon_Name_Translator
{
    public class Language
    {
        public enum Languages
        {
            ja = 1,
            roomaji = 2,
            ko = 3,
            zh = 4,
            fr = 5,
            de = 6,
            es = 7,
            it = 8,
            en = 9,
            cs = 10,
            kanji = 11
        }
    }
}

path: Pokémon Name Translator/Language.cs


code:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Pokémon_Name_Translator
{
    public class Move : IBaseObject
    {
        private static readonly int[] SUPPORTED_LANGUAGES = { 1, 3, 5, 6, 7, 8, 9 };

        public int Id { get; set; }

        public string InputName { get; set; }
        public string EnglishName { get; set; }
        public string Name { get; set; }

        public string NameWithFallback
        {
            get
            {
                if (!string.IsNullOrEmpty(Name))
                    return Name;
                else
                    return InputName;
            }
        }

        public Type Type { get; set; }

        public string IconPlusName
        {
            get
            {
                return $"<img src='{Type.Image}' style='width:30px;height:30px'/>{NameWithFallback}"; ;
            }
        }

        public static List<Move> GetAll(Language.Languages inputLanguage, Language.Languages outputLanguage)
        {
            var Types = new TypeDictionary(inputLanguage, outputLanguage);
            Dictionary<int, Move> moveDict = new Dictionary<int, Move>();
            List<Move> output = new List<Move>();
            //input = ToTitle(input);
            var moveLines = File.ReadAllLines(Constants.FILE_MOVES);
            var moveNamesLines = File.ReadAllLines(Constants.FILE_MOVE_NAMES);

            int LINE_COUNT = SUPPORTED_LANGUAGES.Length;
            if (!SUPPORTED_LANGUAGES.Contains((int)inputLanguage))
                throw new IndexOutOfRangeException("Input language not supported.");
            if (!SUPPORTED_LANGUAGES.Contains((int)outputLanguage))
                throw new IndexOutOfRangeException("Output language not supported.");

            //Meta-Daten der Attacken einlesen
            for (int lineNr = 1; lineNr < moveLines.Length; lineNr++)
            {
                string[] moveSplit = moveLines[lineNr].Split(',');

                Move m = new Move();
                m.Id = Convert.ToInt32(moveSplit[0]);
                m.Type = Types.Types[Convert.ToInt32(moveSplit[3])];
                moveDict.Add(m.Id, m);
            }

            //Lokalisierung
            int currentMoveId = 0;
            bool hasInputName = false;
            bool hasOutputName = false;
            bool hasEngName = false;
            for (int lineNr = 1; lineNr < moveNamesLines.Length; lineNr ++)
            {
                string[] split = moveNamesLines[lineNr].Split(',');
                int moveId = Convert.ToInt32(split[0]);

                if (currentMoveId != moveId)
                {
                    currentMoveId = moveId;
                    hasInputName = hasOutputName = hasEngName = false;
                }

                if (!hasInputName || !hasOutputName || !hasEngName)
                {
                    int rowLangId = Convert.ToInt32(split[1]);
                    Move currentMove = moveDict[moveId];

                    if (rowLangId == (int)inputLanguage)
                    {
                        hasInputName = true;
                        currentMove.InputName = split[2];
                    }
                    if (rowLangId == (int)outputLanguage)
                    {
                        hasOutputName = true;
                        currentMove.Name = split[2];
                    }
                    if (rowLangId == (int)Language.Languages.en)
                    {
                        hasEngName = true;
                        currentMove.EnglishName = split[2];
                    }

                    if (hasInputName && hasOutputName && hasEngName)
                        output.Add(currentMove);
                }
            }
            return output;
        }

    }
}

path: Pokémon Name Translator/Move.cs


code:
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{0E339E73-189A-475F-AD14-3915A82D6494}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>Pokémon_Name_Translator</RootNamespace>
    <AssemblyName>Pokémon Name Translator</AssemblyName>
    <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
    <PublishUrl>publish\</PublishUrl>
    <Install>true</Install>
    <InstallFrom>Disk</InstallFrom>
    <UpdateEnabled>false</UpdateEnabled>
    <UpdateMode>Foreground</UpdateMode>
    <UpdateInterval>7</UpdateInterval>
    <UpdateIntervalUnits>Days</UpdateIntervalUnits>
    <UpdatePeriodically>false</UpdatePeriodically>
    <UpdateRequired>false</UpdateRequired>
    <MapFileExtensions>true</MapFileExtensions>
    <ApplicationRevision>0</ApplicationRevision>
    <ApplicationVersion>1.0.0.%2a</ApplicationVersion>
    <IsWebBootstrapper>false</IsWebBootstrapper>
    <UseApplicationTrust>false</UseApplicationTrust>
    <BootstrapperEnabled>true</BootstrapperEnabled>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <PlatformTarget>AnyCPU</PlatformTarget>
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System.Data" />
    <Reference Include="System.Net.Http" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Helper.cs" />
    <Compile Include="IBaseObject.cs" />
    <Compile Include="Constants.cs" />
    <Compile Include="Language.cs" />
    <Compile Include="Move.cs" />
    <Compile Include="Pokémon.cs" />
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <Compile Include="Type.cs" />
    <Compile Include="TypeDictionary.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="App.config" />
    <None Include="Build.zip" />
    <None Include="Templates\move_names.csv" />
    <None Include="Templates\type_names.csv" />
  </ItemGroup>
  <ItemGroup>
    <None Include="Templates\moves.csv" />
    <None Include="Templates\pokemon_species_names.csv" />
  </ItemGroup>
  <ItemGroup>
    <Content Include="LICENSE.txt" />
    <Content Include="Templates\FoxReplace\Post.txt" />
    <Content Include="Templates\FoxReplace\Pre.txt" />
    <Content Include="Templates\FoxReplace\Repeat.txt" />
    <Content Include="Templates\FoxReplace\Separator.txt" />
    <Content Include="Templates\Userscript\Post.txt" />
    <Content Include="Templates\Userscript\Pre.txt" />
    <Content Include="Templates\Userscript\Repeat.txt" />
    <Content Include="Templates\Userscript\Separator.txt" />
  </ItemGroup>
  <ItemGroup>
    <BootstrapperPackage Include=".NETFramework,Version=v4.5.2">
      <Visible>False</Visible>
      <ProductName>Microsoft .NET Framework 4.5.2 %28x86 and x64%29</ProductName>
      <Install>true</Install>
    </BootstrapperPackage>
    <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
      <Visible>False</Visible>
      <ProductName>.NET Framework 3.5 SP1</ProductName>
      <Install>false</Install>
    </BootstrapperPackage>
  </ItemGroup>
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <PropertyGroup>
    <PostBuildEvent>xcopy "$(ProjectDir)Templates" "$(TargetDir)Templates\" /s /y</PostBuildEvent>
  </PropertyGroup>
  <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
       Other similar extension points exist, see Microsoft.Common.targets.
  <Target Name="BeforeBuild">
  </Target>
  <Target Name="AfterBuild">
  </Target>
  -->
</Project>

path: Pokémon Name Translator/Pokémon Name Translator.csproj


code:
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization;

namespace Pokémon_Name_Translator
{
    public class Pokémon : IBaseObject
    {
        private static readonly int[] SUPPORTED_LANGUAGES = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        public int Id { get; set; }
        public string InputName { get; set; }
        public string EnglishName { get; set; }
        public string Name { get; set; }

        public string NameWithFallback
        {
            get
            {
                if (!string.IsNullOrEmpty(Name))
                    return Name;
                else
                    return InputName;
            }
        }

        public string IconPlusName
        {
            get
            {
                return $"<img src='https://raw.githubusercontent.com/msikma/pokesprite/master/icons/pokemon/regular/{EnglishName.ToLower()}.png'/>{NameWithFallback}";
            }
        }

        public static List<Pokémon> GetAll(Language.Languages inputLanguage, Language.Languages outputLanguage)
        {
            List<Pokémon> output = new List<Pokémon>();
            var lines = File.ReadAllLines(Constants.FILE_POKEMON_SPECIES_NAMES);

            int LINE_COUNT = SUPPORTED_LANGUAGES.Length;
            if (!SUPPORTED_LANGUAGES.Contains((int)inputLanguage))
                throw new IndexOutOfRangeException("Input language not supported.");
            if (!SUPPORTED_LANGUAGES.Contains((int)outputLanguage))
                throw new IndexOutOfRangeException("Output language not supported.");

            int currentSpeciesId = 0;
            bool hasInputName = false;
            bool hasOutputName = false;
            bool hasEngName = false;
            string inputName = "";
            string outputName = "";
            string engName = "";
            for (int lineNr = 1; lineNr < lines.Length; lineNr++)
            {
                string[] split = lines[lineNr].Split(',');
                int speciesId = Convert.ToInt32(split[0]);

                if (currentSpeciesId != speciesId)
                {
                    currentSpeciesId = speciesId;
                    hasInputName = hasOutputName = hasEngName = false;
                }

                if (!hasInputName || !hasOutputName || !hasEngName)
                {
                    int rowLangId = Convert.ToInt32(split[1]);
                    if (rowLangId == (int)inputLanguage)
                    {
                        hasInputName = true;
                        inputName = split[2];
                    }
                    if (rowLangId == (int)outputLanguage)
                    {
                        hasOutputName = true;
                        outputName = split[2];
                    }
                    if (rowLangId == (int)Language.Languages.en)
                    {
                        hasEngName = true;
                        engName = split[2];
                    }

                    if (hasInputName && hasOutputName && hasEngName)
                        output.Add(new Pokémon() { Id = speciesId, InputName = inputName, Name = outputName, EnglishName = engName });
                }
            }
            return output;
        }

    }
}

path: Pokémon Name Translator/Pokémon.cs


code:
//Datenquelle: https://github.com/veekun/pokedex/blob/master/pokedex/data/csv

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Linq;
using System.IO;

namespace Pokémon_Name_Translator
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.Write("Enter [1] Output Pokémon Names, [2] Output Pokémon Moves: ");
            try
            {
                switch (Console.ReadKey().KeyChar)
                {
                    case '1':
                        PrintPokémonNames();
                        break;
                    case '2':
                        PrintMoveNames();
                        break;
                }
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.WriteLine();
            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }

        private static void PrintPokémonNames()
        {
            var languages = GetLanguages();
            var template = GetTemplate();
            Console.Write("\r\nLoading Pokémon names...");
            List<Pokémon> pokémon = Pokémon.GetAll(languages.Input, languages.Output);
            Console.Write("done");

            Print(pokémon.Cast<IBaseObject>().ToList(), template, "PokémonNames.txt", "Pokémon Names");
        }

        private static void PrintMoveNames()
        {
            var languages = GetLanguages();
            var template = GetTemplate();
            Console.Write("\r\nLoading move names...");
            List<Move> moves = Move.GetAll(languages.Input, languages.Output);
            Console.Write("done");

            Print(moves.Cast<IBaseObject>().ToList(), template, "MoveNames.txt", "Move Names");
        }

        private static void Print(List<IBaseObject> all, string templateDir, string fileName, string title)
        {
            Console.Write("\r\nPrepare...");
            using (var outputStream = File.Open(fileName, FileMode.Create))
            {
                using (var writer = new StreamWriter(outputStream))
                {
                    var pre = File.ReadAllText(templateDir + Constants.FILE_PRE).Replace(Constants.REPLACE_NAME, title);
                    var repeat = File.ReadAllText(templateDir + Constants.FILE_REPEAT);
                    var separator = File.ReadAllText(templateDir + Constants.FILE_SEPARATOR);
                    var post = File.ReadAllText(templateDir + Constants.FILE_POST);
                    writer.Write(pre);
                    Console.Write("done");

                    Console.Write("\r\nWrite entries...");
                    int lastPercent = 0;
                    for (int i = 0; i < all.Count; i++)
                    {
                        int percent = (int)Math.Round((double)i / (double)all.Count * (double)100);
                        if (percent != lastPercent)
                        {
                            lastPercent = percent;
                            Console.SetCursorPosition(0, 8);
                            Console.Write("Write entries..." + percent + "%");
                        }

                        if (i > 0)
                            writer.Write(separator);

                        writer.Write(repeat.Replace(Constants.REPLACE_INPUT, all[i].InputName)
                            .Replace(Constants.REPLACE_OUTPUT, all[i].IconPlusName));
                    }
                    Console.SetCursorPosition(0, 8);
                    Console.Write("Write entries...done");

                    Console.Write("\r\nFinish...");
                    writer.Write(post);
                }
            }
            Console.Write("done");
        }

        //private interface ITzz
        //{
        //    string value { get; set; }
        //}
        //private class Tzz : ITzz
        //{
        //    public string value { get; set; }
        //}
        //private class Tuu : ITzz
        //{
        //    public string value { get; set; }
        //}

        private static string GetTemplate()
        {
            string output;
            Console.WriteLine("\r\nEnter target template [1] FoxReplace, [2] Userscript: ");

            switch (Console.ReadKey().KeyChar)
            {
                case '1':
                    output = "FoxReplace";
                    break;
                case '2':
                    output = "Userscript";
                    break;
                default:
                    throw new IndexOutOfRangeException();
            }
            return "Templates\\" + output + "\\";
        }

        private static Languages GetLanguages()
        {
            Languages returnValue = new Languages();

            Console.WriteLine("\r\nAvailable languages: ");
            var languages = (Language.Languages[])Enum.GetValues(typeof(Language.Languages));
            for (int i = 0; i < languages.Length; i++)
            {
                Console.Write("[{0}] {1}", i, languages[i]);
            }
            Console.Write("\r\nEnter input language number: ");
            returnValue.Input = (Language.Languages)Convert.ToInt32(Console.ReadKey().KeyChar.ToString());

            Console.Write("\r\nEnter output language number: ");
            returnValue.Output = (Language.Languages)Convert.ToInt32(Console.ReadKey().KeyChar.ToString());

            return returnValue;
        }

        private struct Languages
        {
            public Language.Languages Input;
            public Language.Languages Output;
        }

        //static void WriteOnBottomLine(string text)
        //{
        //    int x = Console.CursorLeft;
        //    int y = Console.CursorTop;
        //    Console.CursorTop = Console.WindowTop + Console.WindowHeight - 1;
        //    Console.Write(text);
        //    // Restore previous position
        //    Console.SetCursorPosition(x, y);
        //}

        private static void PrintPokémonMoves()
        {
            throw new NotImplementedException();
        }
    }
}

path: Pokémon Name Translator/Program.cs


code:
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Pokémon Name Translator")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Pokémon Name Translator")]
[assembly: AssemblyCopyright("Copyright ©  2016 Matthias Porges")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]

// Setting ComVisible to false makes the types in this assembly not visible
// to COM components.  If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]

// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("0e339e73-189a-475f-ad14-3915a82d6494")]

// Version information for an assembly consists of the following four values:
//
//      Major Version
//      Minor Version
//      Build Number
//      Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]


code:
      ],
      "html": "output",
      "enabled": true
    }
  ]
}


code:
{
  "version": "0.15",
  "groups": [
    {
      "name": "%NAME%",
      "urls": [],
      "substitutions": [


input: %INPUT%

inputType: wholewords

output: %OUTPUT%


code:
    ///////////////////////////////////////////////////////
    };

    //////////////////////////////////////////////////////////////////////////////
    // This is where the real code is
    // Don't edit below this
    //////////////////////////////////////////////////////////////////////////////

    var regexs = [], replacements = [],
        tagsWhitelist = ['PRE', 'BLOCKQUOTE', 'CODE', 'INPUT', 'BUTTON', 'TEXTAREA'],
        rIsRegexp = /^\/(.+)\/([gim]+)?$/,
        word, text, texts, i, userRegexp;

    // prepareRegex by JoeSimmons
    // used to take a string and ready it for use in new RegExp()
    function prepareRegex(string) {
        return string.replace(/([\[\]\^\&\$\.\(\)\?\/\\\+\{\}\|])/g, '\\$1');
    }

    // function to decide whether a parent tag will have its text replaced or not
    function isTagOk(tag) {
        return tagsWhitelist.indexOf(tag) === -1;
    }

    delete words['']; // so the user can add each entry ending with a comma,
                      // I put an extra empty key/value pair in the object.
                      // so we need to remove it before continuing

    // convert the 'words' JSON object to an Array
    for (word in words) {
        if ( typeof word === 'string' && words.hasOwnProperty(word) ) {
            userRegexp = word.match(rIsRegexp);

            // add the search/needle/query
            if (userRegexp) {
                regexs.push(
                    new RegExp(userRegexp[1], userRegexp[2])
                );
            } else {
                regexs.push(
                    new RegExp(prepareRegex(word).replace(/\\?\*/g, function (fullMatch) {
                        return fullMatch === '\\*' ? '*' : '[^ ]*';
                    }), 'g')
                );
            }

            // add the replacement
            replacements.push( words[word] );
        }
    }

    // do the replacement
    texts = document.evaluate('//body//text()[ normalize-space(.) != "" ]', document, null, 6, null);
    for (i = 0; text = texts.snapshotItem(i); i += 1) {
        if (text.parentNode !== null && typeof text.parentNode.innerHTML !== 'undefined' && isTagOk(text.parentNode.tagName) ) {
            var txt = text.parentNode.innerHTML;
            regexs.forEach(function (value, index) {
                txt = txt.replace(value, "$1" + replacements[index] + "$2" );
            });
            text.parentNode.innerHTML = txt;
        }
    }

}());


code:
// ==UserScript==
// @name           Replace Text On Webpages
// @namespace      http://userscripts.org/users/23652
// @description    Replaces text on websites. Now supports wildcards in search queries. Won't replace text in certain tags like links and code blocks
// @include        http://*
// @include        https://*
// @include        file://*
// @exclude        http://userscripts.org/scripts/review/*
// @exclude        http://userscripts.org/scripts/edit/*
// @exclude        http://userscripts.org/scripts/edit_src/*
// @exclude        https://userscripts.org/scripts/review/*
// @exclude        https://userscripts.org/scripts/edit/*
// @exclude        https://userscripts.org/scripts/edit_src/*
// @copyright      JoeSimmons
// @version        1.1.0
// @license        http://creativecommons.org/licenses/by-nc-nd/3.0/us/
// @downloadURL    http://userscripts.org/scripts/source/41369.user.js
// @updateURL      http://userscripts.org/scripts/source/41369.meta.js
// ==/UserScript==
(function () {
    'use strict';

    /*
        NOTE:
            You can use \\* to match actual asterisks instead of using it as a wildcard!
            The examples below show a wildcard in use and a regular asterisk replacement.
    */

    var words = {
    ///////////////////////////////////////////////////////

        // Syntax: 'Search word' : 'Replace word',

		// Pokémon Name Translator