diff --git a/bun.lock b/bun.lock
index 485550a..f062eca 100644
--- a/bun.lock
+++ b/bun.lock
@@ -1,6 +1,5 @@
{
"lockfileVersion": 1,
- "configVersion": 0,
"workspaces": {
"": {
"name": "vue-boilerplate",
@@ -8,6 +7,7 @@
"@tailwindcss/vite": "^4.3.1",
"daisyui": "^5.5.23",
"vue": "^3.5.38",
+ "vue-i18n": "11",
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
@@ -171,6 +171,14 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+ "@intlify/core-base": ["@intlify/core-base@11.4.5", "", { "dependencies": { "@intlify/devtools-types": "11.4.5", "@intlify/message-compiler": "11.4.5", "@intlify/shared": "11.4.5" } }, "sha512-lja3F/iKVIvTa48mIwmrIeDcQUFZ0F0drvFvT8AwINOvbwnAzl/S/p8p2DxILZpWEUHRi1qewfWNIkMvhD3kKA=="],
+
+ "@intlify/devtools-types": ["@intlify/devtools-types@11.4.5", "", { "dependencies": { "@intlify/core-base": "11.4.5", "@intlify/shared": "11.4.5" } }, "sha512-W5vydP9Yq3t82IyWqCM6aR0BTWCZrN5RAwjZEPpH8I2OQWp2RLy03Evh2ANZlSMhcvGAoyDg25k0so85Kwncpw=="],
+
+ "@intlify/message-compiler": ["@intlify/message-compiler@11.4.5", "", { "dependencies": { "@intlify/shared": "11.4.5", "source-map-js": "^1.0.2" } }, "sha512-IEOZiHtbQopyPc/Dz2M869lOlZYX1SdcniNJwphATDYHhovvIneEKf1EFF37DE7NAABZtza1FNtnwwqZWInfpw=="],
+
+ "@intlify/shared": ["@intlify/shared@11.4.5", "", {}, "sha512-g/i5mtdUa9ia/8BaJ4w6ZRHgAXYQd9XyCaQPRMvsd8d5qmZwkjoTmHrNsI28Q/7I8h+2ijUkI4uEnnMCziKupQ=="],
+
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -383,6 +391,8 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.38", "", { "dependencies": { "@vue/compiler-dom": "3.5.38", "@vue/shared": "3.5.38" } }, "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA=="],
+ "@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
+
"@vue/devtools-core": ["@vue/devtools-core@8.1.3", "", { "dependencies": { "@vue/devtools-kit": "^8.1.3", "@vue/devtools-shared": "^8.1.3" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-xezkv5/CPH/o5C8PE2Len9MnTJMsctYYQbKbbUiNOJpKd+fRHj27nKDb/sbtYI8NSQduegeQhCJGKRgAiOV6Uw=="],
"@vue/devtools-kit": ["@vue/devtools-kit@8.1.3", "", { "dependencies": { "@vue/devtools-shared": "^8.1.3", "birpc": "^2.6.1", "hookable": "^5.5.3", "perfect-debounce": "^2.0.0" } }, "sha512-cRn7GXiCQkMYU2Z3h3pM4YO/ndbx9FY1yLDAqIqPLcmIq4H6zAOJHein6tvZU3AfPwgrodqLiPBEF+YQaS8AxA=="],
@@ -833,6 +843,8 @@
"vue-eslint-parser": ["vue-eslint-parser@10.4.0", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0 || ^9.0.0", "eslint-visitor-keys": "^4.2.0 || ^5.0.0", "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg=="],
+ "vue-i18n": ["vue-i18n@11.4.5", "", { "dependencies": { "@intlify/core-base": "11.4.5", "@intlify/devtools-types": "11.4.5", "@intlify/shared": "11.4.5", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-rm8YJ6RpjOrkcgS2GLrZwLvs/VbhxbTSuEspbyXDo233+fPK0OMFNLOj3fdQYVKdOgcpSfLW91JhbqgpkkcBWA=="],
+
"vue-tsc": ["vue-tsc@3.3.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.3.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-Rzh/G2MmNlMSAMTiQEjDrsb4dgB/jbtEM47rVN2NtidF1dfb/q4w4QvpQBtW5+y3y5H27Hjh7deVwk+YB02fNg=="],
"which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="],
diff --git a/package.json b/package.json
index 2b4ecb3..c04d9e1 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"dependencies": {
"@tailwindcss/vite": "^4.3.1",
"daisyui": "^5.5.23",
- "vue": "^3.5.38"
+ "vue": "^3.5.38",
+ "vue-i18n": "11"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
diff --git a/src/App.vue b/src/App.vue
index 95107d6..61d8d51 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,15 +1,11 @@
@@ -36,27 +26,40 @@ onMounted(() => {
type="radio"
name="app_tabs"
class="tab"
- :aria-label="lang === 'french' ? 'Aléatoire' : 'Random'"
+ :aria-label="$t('tab.random')"
checked
/>
-
+
+
+
+
+
diff --git a/src/components/CoolDomain.vue b/src/components/CoolDomain.vue
index 4e977b8..c96e4a1 100644
--- a/src/components/CoolDomain.vue
+++ b/src/components/CoolDomain.vue
@@ -1,16 +1,16 @@
-
-
- {{ domain }}
+ {{ domain.name }}
diff --git a/src/components/PageHistory.vue b/src/components/PageHistory.vue
new file mode 100644
index 0000000..f6fe9bd
--- /dev/null
+++ b/src/components/PageHistory.vue
@@ -0,0 +1,30 @@
+
+
+
+
+
diff --git a/src/components/PageInput.vue b/src/components/PageInput.vue
index d5fce47..3b771fc 100644
--- a/src/components/PageInput.vue
+++ b/src/components/PageInput.vue
@@ -2,15 +2,19 @@
import { ref } from "vue";
import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue";
+import type { Domain } from "@/types";
+import { isDomainAvailable } from "@/lib/whois";
-const props = defineProps<{ lang: string; factory: DomainFactory }>();
+const props = defineProps<{ factory: DomainFactory }>();
const input = ref
("");
-const domains = ref([]);
+const domains = ref([]);
+const noDomain = ref(false);
function start() {
+ noDomain.value = false;
+ domains.value.splice(0, domains.value.length);
if (input.value.length) {
- domains.value.splice(0, domains.value.length);
newDomain();
}
}
@@ -18,15 +22,17 @@ function start() {
function newDomain() {
const startInput = input.value;
props.factory
- .nextFromWord(input.value)
- .then((domain) => {
+ .allFromWord(input.value)
+ .then((newDomains: Domain[]) => {
if (input.value === startInput) {
- domains.value.push(domain);
- newDomain();
+ domains.value.push(...newDomains);
+ domains.value.forEach((domain) => void isDomainAvailable(domain));
}
})
.catch(() => {
- // TODO handle 0 domains
+ if (input.value === startInput && !domains.value.length) {
+ noDomain.value = true;
+ }
});
}
@@ -37,25 +43,34 @@ function newDomain() {
-
+
+
+ {{ $t("seems") }}
+ {{ $t("available") }} {{ $t("is_unavailable") }}
+
+
+ {{ $t("page_input.not_found") }}
diff --git a/src/components/PageRandom.vue b/src/components/PageRandom.vue
index 516e227..74e56c0 100644
--- a/src/components/PageRandom.vue
+++ b/src/components/PageRandom.vue
@@ -3,6 +3,8 @@ import { ref, onBeforeMount, watch } from "vue";
import { isDomainAvailable } from "@/lib/whois";
import type { DomainFactory } from "@/lib/factory";
import CoolDomain from "./CoolDomain.vue";
+import type { Domain } from "@/types";
+import { useI18n } from "vue-i18n";
enum State {
Loading,
@@ -12,19 +14,20 @@ enum State {
Error,
}
-const props = defineProps<{ lang: string; factory: DomainFactory }>();
+const props = defineProps<{ factory: DomainFactory }>();
-const domain = ref(null);
-const domainAvailable = ref(false);
+const domain = ref(null);
const state = ref(State.Loading);
+const i18n = useI18n();
+
function newDomain() {
if (!props.factory.error) {
state.value = State.Generating;
props.factory
- .next(props.lang)
- .then((value: string) => {
- domain.value = value;
+ .next(i18n.locale.value)
+ .then((newDomain: Domain) => {
+ domain.value = newDomain;
checkAvailability();
})
.catch((err: unknown) => {
@@ -37,13 +40,13 @@ function newDomain() {
function checkAvailability() {
if (domain.value) {
const promiseDomain = domain.value;
+ domain.value.searching = true;
state.value = State.Fetching;
- isDomainAvailable(promiseDomain)
+ isDomainAvailable(domain.value)
.then((available: boolean) => {
- if (domain.value !== promiseDomain) {
+ if (domain.value?.name !== promiseDomain.name) {
return; // domain changed
}
- domainAvailable.value = available;
if (!available) {
newDomain();
} else {
@@ -58,86 +61,58 @@ function checkAvailability() {
}
function getTextClass() {
- if (!domain.value || domain.value.length < 15) {
+ if (!domain.value || domain.value.name.length < 15) {
return "text-5xl";
}
- if (domain.value.length < 20) {
+ if (domain.value.name.length < 20) {
return "text-4xl";
}
- if (domain.value.length < 25) {
+ if (domain.value.name.length < 25) {
return "text-3xl";
}
- if (domain.value.length < 30) {
+ if (domain.value.name.length < 30) {
return "text-2xl";
}
return "text-xl";
}
-onBeforeMount(() => {
- newDomain();
-});
-watch(() => props.lang, newDomain);
+onBeforeMount(newDomain);
+watch(i18n.locale, newDomain);
-
+
- Chargement de la Cool Domain Factory™
- Loading the Cool Domain Factory™
+ {{ $t("page_random.state.loading") }}
- Création d'un domaine cool
- Generating a new cool domain
+ {{ $t("page_random.state.generating") }}
- Vérification de la disponibilité
- Checking domain availability
+ {{ $t("page_random.state.fetching") }}
-
-
-
Ce domaine n'est pas disponible.
-
This domain is not available.
+
+ {{ $t("this_domain") }} {{ $t("is_unavailable") }}
- Une erreur est survenue, merci de recharger la page.
- An error has occured, please reload the page.
+ {{ $t("error") }}
diff --git a/src/i18n/en.json b/src/i18n/en.json
new file mode 100644
index 0000000..7653850
--- /dev/null
+++ b/src/i18n/en.json
@@ -0,0 +1,27 @@
+{
+ "tab": {
+ "random": "Random",
+ "input": "Input",
+ "history": "History"
+ },
+ "this_domain": "This domain",
+ "is_unavailable": "is not available.",
+ "seems": "seems",
+ "available": "available",
+ "error": "An error has occured, please reload the page.",
+ "page_random": {
+ "state": {
+ "loading": "Loading the Cool Domain Factory™",
+ "generating": "Generating a new cool domain",
+ "fetching": "Checking domain availability"
+ }
+ },
+ "page_input": {
+ "not_found": "No matching domain found.",
+ "placeholder": "Type a word or a phrase"
+ },
+ "button": {
+ "new_domain": "New domain",
+ "generate": "Generate"
+ }
+}
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
new file mode 100644
index 0000000..4af8464
--- /dev/null
+++ b/src/i18n/fr.json
@@ -0,0 +1,27 @@
+{
+ "tab": {
+ "random": "Aléatoire",
+ "input": "Saisie",
+ "history": "Historique"
+ },
+ "this_domain": "Ce domaine",
+ "is_unavailable": "n'est pas disponible.",
+ "seems": "semble être",
+ "available": "disponible",
+ "error": "Une erreur est survenue, merci de recharger la page.",
+ "page_random": {
+ "state": {
+ "loading": "Chargement de la Cool Domain Factory™",
+ "generating": "Création d'un domaine cool",
+ "fetching": "Vérification de la disponibilité"
+ }
+ },
+ "page_input": {
+ "not_found": "Aucun domaine correspondant.",
+ "placeholder": "Écrit un mot ou une phrase."
+ },
+ "button": {
+ "new_domain": "Nouveau domaine",
+ "generate": "Génerer"
+ }
+}
diff --git a/src/lib/factory.ts b/src/lib/factory.ts
index 05c2dd7..4f22bba 100644
--- a/src/lib/factory.ts
+++ b/src/lib/factory.ts
@@ -2,6 +2,7 @@ import rawTldList from "@/data/tld.txt?raw";
import englishWords from "@/data/english.txt?raw";
import frenchWords from "@/data/french.txt?raw";
import { randomElement, shuffled } from "./random";
+import type { Domain } from "@/types";
const TLD_LIST: string[] = rawTldList.trim().split("\n");
@@ -9,10 +10,11 @@ export class DomainFactory {
error: string | null = null;
words: Record
= {};
generated: string[] = [];
+ history: Domain[] = [];
init() {
- this.words.french = this.initWords(frenchWords);
- this.words.english = this.initWords(englishWords);
+ this.words.fr = this.initWords(frenchWords);
+ this.words.en = this.initWords(englishWords);
}
escapeWord(word: string): string {
@@ -58,7 +60,7 @@ export class DomainFactory {
return null;
}
- next(lang: string): Promise {
+ next(lang: string): Promise {
return new Promise((resolve, reject) => {
let k = 10000;
while (k > 0) {
@@ -66,7 +68,9 @@ export class DomainFactory {
const word = this.randomWord(lang);
const domain = this.randomDomain(word);
if (domain) {
- resolve(domain);
+ const output = { name: domain, searching: false, available: false };
+ this.history.push(output);
+ resolve(output);
return;
}
}
@@ -75,18 +79,27 @@ export class DomainFactory {
});
}
- nextFromWord(word: string): Promise {
+ allFromWord(word: string): Promise {
const escapedWord = this.escapeWord(word.replace(/ /gm, "-"));
return new Promise((resolve, reject) => {
- let k = 10000;
- while (k > 0) {
- k--;
- const domain = this.randomDomain(escapedWord);
- if (domain) {
- resolve(domain);
- return;
+ const domains = [];
+ for (const tld of shuffled(TLD_LIST.slice())) {
+ const tldEscaped: string = tld.replace(".", "");
+ if (
+ escapedWord.endsWith(tldEscaped) &&
+ escapedWord.length >= tldEscaped.length + 2
+ ) {
+ const domain = `${escapedWord.substring(0, escapedWord.length - tldEscaped.length)}.${tld}`;
+ if (!domain.includes("-.")) {
+ const output = { name: domain, searching: false, available: false };
+ this.history.push(output);
+ domains.push(output);
+ }
}
}
+ if (domains.length) {
+ resolve(domains);
+ }
reject(new Error("Could not generate a valid domain"));
});
}
diff --git a/src/lib/whois.ts b/src/lib/whois.ts
index 503d675..b47bec3 100644
--- a/src/lib/whois.ts
+++ b/src/lib/whois.ts
@@ -3,6 +3,7 @@ import type {
WhoIsData,
WhoIsEvent,
WhoIsIpAddresses,
+ Domain,
} from "@/types";
function checkEvents(events: WhoIsEvent[]): boolean {
@@ -26,14 +27,17 @@ function checkWhoIsData(data: WhoIsData): boolean {
);
}
-export async function isDomainAvailable(domain: string): Promise {
+export async function isDomainAvailable(domain: Domain): Promise {
+ domain.searching = true;
try {
const response = await fetch(
- `https://whois.klemek.fr/api/lookup/${domain}`,
+ `https://whois.klemek.fr/api/lookup/${domain.name}`,
);
const content: WhoIsResponse = await response.json();
- return checkWhoIsData(content.data);
+ domain.available = checkWhoIsData(content.data);
} catch {
- return false;
+ domain.available = false;
}
+ domain.searching = false;
+ return domain.available;
}
diff --git a/src/main.ts b/src/main.ts
index b670de8..f640e7a 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,4 +1,27 @@
import { createApp } from "vue";
+import { createI18n } from "vue-i18n";
import App from "./App.vue";
+import en from "./i18n/en.json";
+import fr from "./i18n/fr.json";
-createApp(App).mount("#app");
+function getLang() {
+ return document.location.hash === "#french" ? "fr" : "en";
+}
+
+const i18n = createI18n({
+ locale: getLang(),
+ fallbackLocale: "en",
+ messages: {
+ en: en,
+ fr: fr,
+ },
+});
+
+setInterval(() => {
+ const newLang = getLang();
+ if (newLang !== i18n.global.locale) {
+ i18n.global.locale = newLang;
+ }
+}, 100);
+
+createApp(App).use(i18n).mount("#app");
diff --git a/src/types.ts b/src/types.ts
index e7136e3..e1111f5 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -31,3 +31,9 @@ export interface WhoIsEntity {
roles: string[];
vcardArray: [string, [string, object, string, string][]];
}
+
+export interface Domain {
+ name: string;
+ available: boolean;
+ searching: boolean;
+}