Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
/**
* Apply English-value → locale phrase map across all packs (force update).
* Phrase map: scripts/phrase-map.json { "English": { es, fr, de, it, pt, nl, pl, ja } }
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { SAME_AS_EN } from "./locale-extra.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.join(__dirname, "../src/lib/i18n/messages");
const locales = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
const mapPath = path.join(__dirname, "phrase-map.json");
function parseDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
function emitPack(exportName, comment, dict, keyOrder) {
const lines = [
`import type { MessageDict } from "./types";`,
``,
`/** ${comment} */`,
`export const ${exportName}: MessageDict = {`
];
for (const key of keyOrder) {
lines.push(`\t${JSON.stringify(key)}: ${JSON.stringify(dict[key])},`);
}
lines.push(`};`, ``);
return lines.join("\n");
}
const comments = {
es: "Spanish (es) UI pack — keys must stay in sync with en.ts.",
fr: "French (fr) UI pack — keys must stay in sync with en.ts.",
de: "German (de) UI pack — keys must stay in sync with en.ts.",
it: "Italian (it) UI pack — keys must stay in sync with en.ts.",
pt: "Portuguese (pt) UI pack — keys must stay in sync with en.ts.",
nl: "Dutch (nl) UI pack — keys must stay in sync with en.ts.",
pl: "Polish (pl) UI pack — keys must stay in sync with en.ts.",
ja: "Japanese (ja) UI pack — keys must stay in sync with en.ts."
};
if (!fs.existsSync(mapPath)) {
console.error("missing phrase-map.json — run fill scripts first");
process.exit(1);
}
const phraseMap = JSON.parse(fs.readFileSync(mapPath, "utf8"));
const en = parseDict(fs.readFileSync(path.join(messagesDir, "en.ts"), "utf8"));
const keyOrder = Object.keys(en);
let hit = 0;
let miss = 0;
for (const code of locales) {
const existing = parseDict(fs.readFileSync(path.join(messagesDir, `${code}.ts`), "utf8"));
const dict = {};
for (const key of keyOrder) {
const enVal = en[key];
const mapped = phraseMap[enVal]?.[code];
if (typeof mapped === "string" && mapped.trim()) {
dict[key] = mapped;
if (existing[key] === enVal || !existing[key]) hit++;
} else if (typeof existing[key] === "string" && existing[key].trim()) {
dict[key] = existing[key];
} else {
dict[key] = enVal;
if (!SAME_AS_EN.has(key)) miss++;
}
}
fs.writeFileSync(
path.join(messagesDir, `${code}.ts`),
emitPack(code, comments[code], dict, keyOrder),
"utf8"
);
const same = keyOrder.filter((k) => dict[k] === en[k] && !SAME_AS_EN.has(k)).length;
console.log(code, "keys", keyOrder.length, "sameAsEn", same);
}
console.log("phrase hits(approx)", hit, "pads", miss);
+62
View File
@@ -0,0 +1,62 @@
/**
* Build locale-extra-rest.mjs from English→locale phrase map for keys still English in non-es packs.
* Also covers new settings/dashboard strings for all locales including es.
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { EXTRA as EXTRA_ES } from "./locale-extra-es.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
const en = parseMessageDict(
fs.readFileSync(path.resolve(__dirname, "../src/lib/i18n/messages/en.ts"), "utf8")
);
/** English source string → { fr, de, it, pt, nl, pl, ja, es? } */
const BY_EN = JSON.parse(fs.readFileSync(path.join(__dirname, "phrase-map.json"), "utf8"));
const codes = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
const EXTRA = Object.fromEntries(codes.map((c) => [c, {}]));
// 1) Seed non-es from Spanish extras via English lookup of the same key
for (const [key, esText] of Object.entries(EXTRA_ES.es ?? {})) {
const enText = en[key];
if (!enText) continue;
EXTRA.es[key] = esText;
const mapped = BY_EN[enText];
if (!mapped) continue;
for (const code of ["fr", "de", "it", "pt", "nl", "pl", "ja"]) {
if (mapped[code]) EXTRA[code][key] = mapped[code];
}
}
// 2) Apply phrase map to every en key (fills new settings/dashboard too)
for (const [key, enText] of Object.entries(en)) {
const mapped = BY_EN[enText];
if (!mapped) continue;
for (const code of codes) {
if (mapped[code]) EXTRA[code][key] = mapped[code];
}
}
const out = `/** Auto-built by build-phrase-extra.mjs — do not hand-edit; update phrase-map.json. */\nexport const EXTRA = ${JSON.stringify(EXTRA, null, 2)};\n`;
fs.writeFileSync(path.join(__dirname, "locale-extra-rest.mjs"), out, "utf8");
for (const code of codes) {
console.log(code, Object.keys(EXTRA[code]).length);
}
+10
View File
@@ -0,0 +1,10 @@
import { DOCS_GUIDE_TREE, validateDocsGuideTree } from "../src/lib/docs-guide/index.ts";
const errors = validateDocsGuideTree(DOCS_GUIDE_TREE);
if (errors.length > 0) {
console.error(errors.join("\n"));
process.exit(1);
}
console.log(
`docs-guide ok nodes=${Object.keys(DOCS_GUIDE_TREE.nodes).length} version=${DOCS_GUIDE_TREE.version}`
);
+28
View File
@@ -0,0 +1,28 @@
import { createGzip } from "node:zlib";
import { createReadStream, createWriteStream, cpSync, mkdirSync, existsSync, rmSync } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const require = createRequire(import.meta.url);
const here = dirname(fileURLToPath(import.meta.url));
const webRoot = join(here, "..");
const dest = join(webRoot, "static", "vendor", "rapidoc");
const pkgRoot = dirname(require.resolve("rapidoc/package.json"));
const source = join(pkgRoot, "dist", "rapidoc-min.js");
if (!existsSync(source)) {
throw new Error(`rapidoc missing dist/rapidoc-min.js at ${source}`);
}
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
const destJs = join(dest, "rapidoc-min.js");
cpSync(source, destJs);
// Precompress for hooks.server.ts (Accept-Encoding: gzip).
await pipeline(createReadStream(destJs), createGzip({ level: 9 }), createWriteStream(`${destJs}.gz`));
console.log(`Copied rapidoc dist/rapidoc-min.js → ${dest}`);
+254
View File
@@ -0,0 +1,254 @@
/**
* Count keys identical to English across locale packs.
* Brands/loanwords/symbols are excluded from the "unfinished" total.
*
* Run: node apps/web/scripts/count-identical-to-en.mjs
* Optional: --json writes apps/web/scripts/_identical-to-en-report.json
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.resolve(__dirname, "../src/lib/i18n/messages");
const LOCALES = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
const BRAND_VALUES = new Set([
" · upgrade",
" (v{version})",
"—",
".",
"…",
"· upgrade",
"(v{version})",
"{count} file",
"{count} options",
"{credits} credits",
"{used} / {max} SKUs",
"→",
"Account",
"Action",
"Actions",
"Admin",
"AI",
"Alias",
"Allowlist",
"Analytics",
"API",
"Assistant",
"Audience",
"Azure OpenAI",
"Base",
"Black Friday 2026",
"Business",
"Client ID",
"cm",
"colleague@example.com",
"Commerce",
"Compliance",
"CSV",
"Date",
"Description",
"Descrybe",
"Dimension",
"Directory",
"Docs:",
"EAN/GTIN",
"Enterprise",
"EPREL",
"EPREL ID",
"EUR",
"Exact",
"Exports",
"FAQ auto-match",
"Feeds",
"Format",
"Format:",
"format: csv",
"Fuzzy",
"Google OAuth",
"GPS",
"Growth",
"Host",
"https://…",
"IA",
"ID",
"Insight",
"Integration",
"item: {path}",
"Job {id}",
"kg",
"Knowledge",
"Knowledge base",
"Last Updated",
"Legacy",
"Live",
"Mail",
"Mapping",
"Marketing",
"Material",
"Media",
"Model",
"Name (A-Z)",
"Name (Z-A)",
"Namespace",
"No",
"Normalize",
"Notes",
"OAuth, EPREL, Pinecone, Stripe, feeds",
"OK",
"Ollama",
"OpenAI",
"OpenAPI",
"OpenRouter",
"Ops",
"Optional",
"Parent",
"pcs",
"Pinecone",
"Plan:",
"Popular",
"Product",
"Prompt",
"Re: {subject}",
"REST",
"Reviewer",
"reviews",
"Reviews: {status}",
"SEO",
"Service",
"Shopify",
"Single",
"SKU",
"Source",
"source {source}",
"Staff",
"Starter",
"Stripe",
"Sync: {status}",
"Tenant vs platform",
"Tenants · volume",
"Test: {status}",
"Ticket",
"Tickets",
"Timeout",
"TSV",
"Type",
"Uploads",
"URL",
"via Stripe",
"Volume",
"W",
"WooCommerce",
"you@company.com",
]);
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
function isBrandOrLoanword(value) {
const raw = String(value ?? "");
const t = raw.trim();
if (!t) return true;
if (BRAND_VALUES.has(raw) || BRAND_VALUES.has(t)) return true;
if (/^\{[^}]+\}$/.test(t)) return true;
// Short all-caps / code-like tokens kept identical by policy
if (/^[A-Z0-9][A-Z0-9._/-]{0,14}$/.test(t) && t === t.toUpperCase()) return true;
return false;
}
function load(code) {
return parseMessageDict(fs.readFileSync(path.join(messagesDir, `${code}.ts`), "utf8"));
}
const en = load("en");
const enKeys = Object.keys(en);
const byLocale = {};
let totalIdentical = 0;
let totalIdenticalExBrand = 0;
let totalTranslated = 0;
let totalBrandIdentical = 0;
for (const code of LOCALES) {
const pack = load(code);
let identical = 0;
let identicalExBrand = 0;
let brandIdentical = 0;
let translated = 0;
let missing = 0;
const unfinishedSamples = [];
for (const key of enKeys) {
if (!(key in pack)) {
missing += 1;
identicalExBrand += 1;
if (unfinishedSamples.length < 8) unfinishedSamples.push(key);
continue;
}
if (pack[key] === en[key]) {
identical += 1;
if (isBrandOrLoanword(en[key])) {
brandIdentical += 1;
} else {
identicalExBrand += 1;
if (unfinishedSamples.length < 8) unfinishedSamples.push(`${key}=${JSON.stringify(en[key])}`);
}
} else {
translated += 1;
}
}
byLocale[code] = {
keys: Object.keys(pack).length,
identical,
identicalExBrand,
brandIdentical,
translated,
missing,
unfinishedSamples,
};
totalIdentical += identical;
totalIdenticalExBrand += identicalExBrand;
totalBrandIdentical += brandIdentical;
totalTranslated += translated;
console.log(
`${code}: identical=${identical} identicalExBrand=${identicalExBrand} brandIdentical=${brandIdentical} translated=${translated} missing=${missing}`,
);
}
const report = {
enKeys: enKeys.length,
locales: LOCALES,
byLocale,
totals: {
identical: totalIdentical,
identicalExBrand: totalIdenticalExBrand,
brandIdentical: totalBrandIdentical,
translated: totalTranslated,
avgIdenticalExBrandPerLocale: Math.round(totalIdenticalExBrand / LOCALES.length),
},
measuredAt: new Date().toISOString(),
};
console.log("---");
console.log(`enKeys=${report.enKeys}`);
console.log(`TOTAL identical=${totalIdentical}`);
console.log(`TOTAL identicalExBrand (excl brands)=${totalIdenticalExBrand}`);
console.log(`AVG identicalExBrand/locale=${report.totals.avgIdenticalExBrandPerLocale}`);
console.log(`TOTAL brandIdentical=${totalBrandIdentical}`);
console.log(`TOTAL translated=${totalTranslated}`);
if (process.argv.includes("--json")) {
const out = path.join(__dirname, "_identical-to-en-report.json");
fs.writeFileSync(out, JSON.stringify(report, null, 2), "utf8");
console.log(`wrote ${out}`);
}
+18
View File
@@ -0,0 +1,18 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../src/lib/i18n/messages");
function countKeys(file) {
const s = fs.readFileSync(path.join(dir, file), "utf8");
const d = {};
const re = /"([^"\\]+)"\s*:/g;
let m;
while ((m = re.exec(s))) d[m[1]] = 1;
return Object.keys(d).length;
}
for (const c of ["en", "es", "fr", "de", "it", "pt", "nl", "pl", "ja"]) {
console.log(c, countKeys(`${c}.ts`));
}
+45
View File
@@ -0,0 +1,45 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { EXTRA as EXTRA_COMMON, SAME_AS_EN } from "./locale-extra.mjs";
import { EXTRA as EXTRA_ES } from "./locale-extra-es.mjs";
import { EXTRA as EXTRA_REST } from "./locale-extra-rest.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const enPath = path.resolve(__dirname, "../src/lib/i18n/messages/en.ts");
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
// Import PACKS by re-running merge logic inline is hard — load from written es/fr instead.
const en = parseMessageDict(fs.readFileSync(enPath, "utf8"));
const esFile = parseMessageDict(
fs.readFileSync(path.resolve(__dirname, "../src/lib/i18n/messages/es.ts"), "utf8")
);
const frFile = parseMessageDict(
fs.readFileSync(path.resolve(__dirname, "../src/lib/i18n/messages/fr.ts"), "utf8")
);
const esMissing = {};
const frMissing = {};
for (const [k, v] of Object.entries(en)) {
if (SAME_AS_EN.has(k)) continue;
if (esFile[k] === v) esMissing[k] = v;
if (frFile[k] === v) frMissing[k] = v;
}
fs.writeFileSync(path.join(__dirname, "_es-still-en.json"), JSON.stringify(esMissing, null, 2));
fs.writeFileSync(path.join(__dirname, "_fr-still-en.json"), JSON.stringify(frMissing, null, 2));
console.log("es still en", Object.keys(esMissing).length);
console.log("fr still en", Object.keys(frMissing).length);
console.log("en total", Object.keys(en).length);
+25
View File
@@ -0,0 +1,25 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const enPath = path.resolve(__dirname, "../src/lib/i18n/messages/en.ts");
const outPath = path.resolve(__dirname, "_en-dump.json");
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
const en = parseMessageDict(fs.readFileSync(enPath, "utf8"));
fs.writeFileSync(outPath, JSON.stringify(en, null, 2), "utf8");
console.log(`keys=${Object.keys(en).length} -> ${outPath}`);
+199
View File
@@ -0,0 +1,199 @@
import fs from "node:fs";
import { EXTRA as EXTRA_ES } from "./locale-extra-es.mjs";
function parse(s) {
const d = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(s))) {
const raw = m[2];
d[m[1]] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return d;
}
const en = parse(fs.readFileSync("../src/lib/i18n/messages/en.ts", "utf8"));
const seed = JSON.parse(fs.readFileSync("_phrase-seed.json", "utf8"));
/** @type {Record<string, {fr:string,de:string,it:string,pt:string,nl:string,pl:string,ja:string}>} */
const MORE = {
Descrybe: { fr: "Descrybe", de: "Descrybe", it: "Descrybe", pt: "Descrybe", nl: "Descrybe", pl: "Descrybe", ja: "Descrybe" },
"—": { fr: "—", de: "—", it: "—", pt: "—", nl: "—", pl: "—", ja: "—" },
Marketing: { fr: "Marketing", de: "Marketing", it: "Marketing", pt: "Marketing", nl: "Marketing", pl: "Marketing", ja: "マーケティング" },
Exports: { fr: "Exports", de: "Exporte", it: "Esportazioni", pt: "Exportações", nl: "Exports", pl: "Eksporty", ja: "エクスポート" },
SEO: { fr: "SEO", de: "SEO", it: "SEO", pt: "SEO", nl: "SEO", pl: "SEO", ja: "SEO" },
Admin: { fr: "Admin", de: "Admin", it: "Admin", pt: "Admin", nl: "Admin", pl: "Admin", ja: "管理" },
EPREL: { fr: "EPREL", de: "EPREL", it: "EPREL", pt: "EPREL", nl: "EPREL", pl: "EPREL", ja: "EPREL" },
"Re: {subject}": { fr: "Re: {subject}", de: "Re: {subject}", it: "Re: {subject}", pt: "Re: {subject}", nl: "Re: {subject}", pl: "Re: {subject}", ja: "Re: {subject}" },
"Sign in": { fr: "Se connecter", de: "Anmelden", it: "Accedi", pt: "Iniciar sessão", nl: "Inloggen", pl: "Zaloguj się", ja: "ログイン" },
"Use your Descrybe email and password.": { fr: "Utilisez votre e-mail et mot de passe Descrybe.", de: "Verwenden Sie Ihre Descrybe-E-Mail und Ihr Passwort.", it: "Usa la tua email e password Descrybe.", pt: "Utilize o seu e-mail e palavra-passe Descrybe.", nl: "Gebruik uw Descrybe e-mailadres en wachtwoord.", pl: "Użyj adresu e-mail i hasła Descrybe.", ja: "Descrybeのメールアドレスとパスワードを使用してください。" },
"Signing in…": { fr: "Connexion…", de: "Anmeldung…", it: "Accesso in corso…", pt: "A iniciar sessão…", nl: "Bezig met inloggen…", pl: "Logowanie…", ja: "ログイン中…" },
"Login failed": { fr: "Échec de la connexion", de: "Anmeldung fehlgeschlagen", it: "Accesso non riuscito", pt: "Falha no início de sessão", nl: "Inloggen mislukt", pl: "Logowanie nie powiodło się", ja: "ログインに失敗しました" },
"Create company": { fr: "Créer une entreprise", de: "Unternehmen erstellen", it: "Crea azienda", pt: "Criar empresa", nl: "Bedrijf aanmaken", pl: "Utwórz firmę", ja: "会社を作成" },
"Accept invite": { fr: "Accepter l'invitation", de: "Einladung annehmen", it: "Accetta invito", pt: "Aceitar convite", nl: "Uitnodiging accepteren", pl: "Zaakceptuj zaproszenie", ja: "招待を承認" },
"Set password": { fr: "Définir le mot de passe", de: "Passwort festlegen", it: "Imposta password", pt: "Definir palavra-passe", nl: "Wachtwoord instellen", pl: "Ustaw hasło", ja: "パスワードを設定" },
"Company name": { fr: "Nom de l'entreprise", de: "Unternehmensname", it: "Nome azienda", pt: "Nome da empresa", nl: "Bedrijfsnaam", pl: "Nazwa firmy", ja: "会社名" },
"Create account": { fr: "Créer un compte", de: "Konto erstellen", it: "Crea account", pt: "Criar conta", nl: "Account aanmaken", pl: "Utwórz konto", ja: "アカウントを作成" },
"Creating…": { fr: "Création…", de: "Wird erstellt…", it: "Creazione…", pt: "A criar…", nl: "Bezig met aanmaken…", pl: "Tworzenie…", ja: "作成中…" },
"Registration failed": { fr: "Échec de l'inscription", de: "Registrierung fehlgeschlagen", it: "Registrazione non riuscita", pt: "Falha no registo", nl: "Registratie mislukt", pl: "Rejestracja nie powiodła się", ja: "登録に失敗しました" },
"Already have an account?": { fr: "Vous avez déjà un compte ?", de: "Haben Sie bereits ein Konto?", it: "Hai già un account?", pt: "Já tem uma conta?", nl: "Heeft u al een account?", pl: "Masz już konto?", ja: "すでにアカウントをお持ちですか?" },
"No account?": { fr: "Pas de compte ?", de: "Kein Konto?", it: "Nessun account?", pt: "Sem conta?", nl: "Geen account?", pl: "Brak konta?", ja: "アカウントがありませんか?" },
"Admin → Users": { fr: "Admin → Utilisateurs", de: "Admin → Benutzer", it: "Admin → Utenti", pt: "Admin → Utilizadores", nl: "Admin → Gebruikers", pl: "Admin → Użytkownicy", ja: "管理 → ユーザー" },
"your email": { fr: "votre e-mail", de: "Ihre E-Mail", it: "la tua email", pt: "o seu e-mail", nl: "uw e-mail", pl: "twój e-mail", ja: "あなたのメール" },
Profile: { fr: "Profil", de: "Profil", it: "Profilo", pt: "Perfil", nl: "Profiel", pl: "Profil", ja: "プロフィール" },
Email: { fr: "E-mail", de: "E-Mail", it: "Email", pt: "E-mail", nl: "E-mail", pl: "E-mail", ja: "メール" },
Member: { fr: "Membre", de: "Mitglied", it: "Membro", pt: "Membro", nl: "Lid", pl: "Członek", ja: "メンバー" },
Remove: { fr: "Retirer", de: "Entfernen", it: "Rimuovi", pt: "Remover", nl: "Verwijderen", pl: "Usuń", ja: "削除" },
Promote: { fr: "Promouvoir", de: "Befördern", it: "Promuovi", pt: "Promover", nl: "Promoveren", pl: "Awansuj", ja: "昇格" },
Demote: { fr: "Rétrograder", de: "Herabstufen", it: "Degrada", pt: "Despromover", nl: "Degraderen", pl: "Degraduj", ja: "降格" },
Role: { fr: "Rôle", de: "Rolle", it: "Ruolo", pt: "Papel", nl: "Rol", pl: "Rola", ja: "ロール" },
Status: { fr: "Statut", de: "Status", it: "Stato", pt: "Estado", nl: "Status", pl: "Status", ja: "ステータス" },
Actions: { fr: "Actions", de: "Aktionen", it: "Azioni", pt: "Ações", nl: "Acties", pl: "Akcje", ja: "操作" },
Name: { fr: "Nom", de: "Name", it: "Nome", pt: "Nome", nl: "Naam", pl: "Nazwa", ja: "名前" },
Key: { fr: "Clé", de: "Schlüssel", it: "Chiave", pt: "Chave", nl: "Sleutel", pl: "Klucz", ja: "キー" },
Plan: { fr: "Offre", de: "Plan", it: "Piano", pt: "Plano", nl: "Plan", pl: "Plan", ja: "プラン" },
Used: { fr: "Utilisé", de: "Verbraucht", it: "Usato", pt: "Usado", nl: "Gebruikt", pl: "Użyte", ja: "使用済み" },
Trial: { fr: "Essai", de: "Testphase", it: "Prova", pt: "Teste", nl: "Proef", pl: "Okres próbny", ja: "トライアル" },
Export: { fr: "Exporter", de: "Exportieren", it: "Esporta", pt: "Exportar", nl: "Exporteren", pl: "Eksportuj", ja: "エクスポート" },
"API Keys": { fr: "Clés API", de: "API-Schlüssel", it: "Chiavi API", pt: "Chaves API", nl: "API-sleutels", pl: "Klucze API", ja: "APIキー" },
"Copy link": { fr: "Copier le lien", de: "Link kopieren", it: "Copia link", pt: "Copiar link", nl: "Link kopiëren", pl: "Kopiuj link", ja: "リンクをコピー" },
"Send invite": { fr: "Envoyer l'invitation", de: "Einladung senden", it: "Invia invito", pt: "Enviar convite", nl: "Uitnodiging verzenden", pl: "Wyślij zaproszenie", ja: "招待を送信" },
"Invite user": { fr: "Inviter un utilisateur", de: "Benutzer einladen", it: "Invita utente", pt: "Convidar utilizador", nl: "Gebruiker uitnodigen", pl: "Zaproś użytkownika", ja: "ユーザーを招待" },
"First Name": { fr: "Prénom", de: "Vorname", it: "Nome", pt: "Nome próprio", nl: "Voornaam", pl: "Imię", ja: "名" },
"Last Name": { fr: "Nom", de: "Nachname", it: "Cognome", pt: "Apelido", nl: "Achternaam", pl: "Nazwisko", ja: "姓" },
"Team Members": { fr: "Membres de l'équipe", de: "Teammitglieder", it: "Membri del team", pt: "Membros da equipa", nl: "Teamleden", pl: "Członkowie zespołu", ja: "チームメンバー" },
"Personal Information": { fr: "Informations personnelles", de: "Persönliche Daten", it: "Informazioni personali", pt: "Informação pessoal", nl: "Persoonlijke gegevens", pl: "Dane osobowe", ja: "個人情報" },
"Update your personal details": { fr: "Mettez à jour vos informations personnelles", de: "Aktualisieren Sie Ihre persönlichen Daten", it: "Aggiorna i tuoi dati personali", pt: "Atualize os seus dados pessoais", nl: "Werk uw persoonlijke gegevens bij", pl: "Zaktualizuj swoje dane osobowe", ja: "個人情報を更新" },
"Your first name": { fr: "Votre prénom", de: "Ihr Vorname", it: "Il tuo nome", pt: "O seu nome próprio", nl: "Uw voornaam", pl: "Twoje imię", ja: "名" },
"Your last name": { fr: "Votre nom", de: "Ihr Nachname", it: "Il tuo cognome", pt: "O seu apelido", nl: "Uw achternaam", pl: "Twoje nazwisko", ja: "姓" },
"Profile updated.": { fr: "Profil mis à jour.", de: "Profil aktualisiert.", it: "Profilo aggiornato.", pt: "Perfil atualizado.", nl: "Profiel bijgewerkt.", pl: "Profil zaktualizowany.", ja: "プロフィールを更新しました。" },
"Could not update profile": { fr: "Impossible de mettre à jour le profil", de: "Profil konnte nicht aktualisiert werden", it: "Impossibile aggiornare il profilo", pt: "Não foi possível atualizar o perfil", nl: "Profiel kon niet worden bijgewerkt", pl: "Nie można zaktualizować profilu", ja: "プロフィールを更新できませんでした" },
"Saving…": { fr: "Enregistrement…", de: "Speichern…", it: "Salvataggio…", pt: "A guardar…", nl: "Bezig met opslaan…", pl: "Zapisywanie…", ja: "保存中…" },
"Accepting…": { fr: "Acceptation…", de: "Wird angenommen…", it: "Accettazione…", pt: "A aceitar…", nl: "Bezig met accepteren…", pl: "Akceptowanie…", ja: "承認中…" },
"Open dashboard": { fr: "Ouvrir le tableau de bord", de: "Dashboard öffnen", it: "Apri dashboard", pt: "Abrir painel", nl: "Dashboard openen", pl: "Otwórz panel", ja: "ダッシュボードを開く" },
"Company settings": { fr: "Paramètres de l'entreprise", de: "Unternehmenseinstellungen", it: "Impostazioni azienda", pt: "Definições da empresa", nl: "Bedrijfsinstellingen", pl: "Ustawienia firmy", ja: "会社の設定" },
"Go to sign in": { fr: "Aller à la connexion", de: "Zur Anmeldung", it: "Vai all'accesso", pt: "Ir para início de sessão", nl: "Naar inloggen", pl: "Przejdź do logowania", ja: "ログインへ" },
"Password saved": { fr: "Mot de passe enregistré", de: "Passwort gespeichert", it: "Password salvata", pt: "Palavra-passe guardada", nl: "Wachtwoord opgeslagen", pl: "Hasło zapisane", ja: "パスワードを保存しました" },
"You're on the team": { fr: "Vous faites partie de l'équipe", de: "Sie sind im Team", it: "Fai parte del team", pt: "Já faz parte da equipa", nl: "U bent in het team", pl: "Jesteś w zespole", ja: "チームに参加しました" },
"Enable fields": { fr: "Activer les champs", de: "Felder aktivieren", it: "Abilita campi", pt: "Ativar campos", nl: "Velden inschakelen", pl: "Włącz pola", ja: "フィールドを有効化" },
"Process products": { fr: "Traiter les produits", de: "Produkte verarbeiten", it: "Elabora prodotti", pt: "Processar produtos", nl: "Producten verwerken", pl: "Przetwarzaj produkty", ja: "商品を処理" },
"Connect feed": { fr: "Connecter un flux", de: "Feed verbinden", it: "Collega feed", pt: "Ligar feed", nl: "Feed koppelen", pl: "Podłącz feed", ja: "フィードを接続" },
"Upload CSV": { fr: "Téléverser un CSV", de: "CSV hochladen", it: "Carica CSV", pt: "Carregar CSV", nl: "CSV uploaden", pl: "Prześlij CSV", ja: "CSVをアップロード" },
"View plans": { fr: "Voir les offres", de: "Pläne ansehen", it: "Vedi i piani", pt: "Ver planos", nl: "Plannen bekijken", pl: "Zobacz plany", ja: "プランを見る" },
"Compare plans": { fr: "Comparer les offres", de: "Pläne vergleichen", it: "Confronta i piani", pt: "Comparar planos", nl: "Plannen vergelijken", pl: "Porównaj plany", ja: "プランを比較" },
"Start a job": { fr: "Démarrer une tâche", de: "Job starten", it: "Avvia un processo", pt: "Iniciar uma tarefa", nl: "Een job starten", pl: "Uruchom zadanie", ja: "ジョブを開始" },
"Resume tutorial": { fr: "Reprendre le tutoriel", de: "Tutorial fortsetzen", it: "Riprendi tutorial", pt: "Retomar tutorial", nl: "Tutorial hervatten", pl: "Wznów samouczek", ja: "チュートリアルを再開" },
"Restart tutorial": { fr: "Relancer le tutoriel", de: "Tutorial neu starten", it: "Riavvia tutorial", pt: "Reiniciar tutorial", nl: "Tutorial opnieuw starten", pl: "Uruchom ponownie samouczek", ja: "チュートリアルを再開する" },
"Open products": { fr: "Ouvrir les produits", de: "Produkte öffnen", it: "Apri prodotti", pt: "Abrir produtos", nl: "Producten openen", pl: "Otwórz produkty", ja: "商品を開く" },
"Welcome to {name}": { fr: "Bienvenue sur {name}", de: "Willkommen bei {name}", it: "Benvenuto in {name}", pt: "Bem-vindo a {name}", nl: "Welkom bij {name}", pl: "Witamy w {name}", ja: "{name} へようこそ" },
"Company Settings": { fr: "Paramètres de l'entreprise", de: "Unternehmenseinstellungen", it: "Impostazioni azienda", pt: "Definições da empresa", nl: "Bedrijfsinstellingen", pl: "Ustawienia firmy", ja: "会社の設定" },
"Active company": { fr: "Entreprise active", de: "Aktives Unternehmen", it: "Azienda attiva", pt: "Empresa ativa", nl: "Actief bedrijf", pl: "Aktywna firma", ja: "アクティブな会社" },
"Credits overview": { fr: "Aperçu des crédits", de: "Credit-Übersicht", it: "Panoramica crediti", pt: "Resumo de créditos", nl: "Credits-overzicht", pl: "Przegląd kredytów", ja: "クレジット概要" },
"Company Information": { fr: "Informations sur l'entreprise", de: "Unternehmensinformationen", it: "Informazioni azienda", pt: "Informação da empresa", nl: "Bedrijfsgegevens", pl: "Informacje o firmie", ja: "会社情報" },
"Update your company details": { fr: "Mettez à jour les détails de votre entreprise", de: "Aktualisieren Sie Ihre Unternehmensdaten", it: "Aggiorna i dettagli dell'azienda", pt: "Atualize os detalhes da empresa", nl: "Werk uw bedrijfsgegevens bij", pl: "Zaktualizuj dane firmy", ja: "会社の詳細を更新" },
"Company Name": { fr: "Nom de l'entreprise", de: "Unternehmensname", it: "Nome azienda", pt: "Nome da empresa", nl: "Bedrijfsnaam", pl: "Nazwa firmy", ja: "会社名" },
"Your company name": { fr: "Le nom de votre entreprise", de: "Ihr Unternehmensname", it: "Il nome della tua azienda", pt: "O nome da sua empresa", nl: "Uw bedrijfsnaam", pl: "Nazwa Twojej firmy", ja: "会社名" },
"Content Settings": { fr: "Paramètres de contenu", de: "Inhaltseinstellungen", it: "Impostazioni contenuti", pt: "Definições de conteúdo", nl: "Contentinstellingen", pl: "Ustawienia treści", ja: "コンテンツ設定" },
"Merge products with the same GTIN": { fr: "Fusionner les produits avec le même GTIN", de: "Produkte mit derselben GTIN zusammenführen", it: "Unisci prodotti con lo stesso GTIN", pt: "Unir produtos com o mesmo GTIN", nl: "Producten met dezelfde GTIN samenvoegen", pl: "Scal produkty z tym samym GTIN", ja: "同じGTINの商品をマージ" },
"Email integration": { fr: "Intégration e-mail", de: "E-Mail-Integration", it: "Integrazione email", pt: "Integração de e-mail", nl: "E-mailintegratie", pl: "Integracja e-mail", ja: "メール連携" },
"AI integrations": { fr: "Intégrations IA", de: "KI-Integrationen", it: "Integrazioni IA", pt: "Integrações de IA", nl: "AI-integraties", pl: "Integracje AI", ja: "AI連携" },
"Operator alerts": { fr: "Alertes opérateur", de: "Operator-Benachrichtigungen", it: "Avvisi operatore", pt: "Alertas do operador", nl: "Operator-meldingen", pl: "Alerty operatora", ja: "オペレーターアラート" },
"In-app toasts": { fr: "Toasts dans l'application", de: "In-App-Toasts", it: "Toast in-app", pt: "Toasts na aplicação", nl: "In-app toasts", pl: "Powiadomienia w aplikacji", ja: "アプリ内トースト" },
"Email alerts": { fr: "Alertes e-mail", de: "E-Mail-Benachrichtigungen", it: "Avvisi email", pt: "Alertas por e-mail", nl: "E-mailmeldingen", pl: "Alerty e-mail", ja: "メールアラート" },
"Create API Key": { fr: "Créer une clé API", de: "API-Schlüssel erstellen", it: "Crea chiave API", pt: "Criar chave API", nl: "API-sleutel maken", pl: "Utwórz klucz API", ja: "APIキーを作成" },
"Create API key": { fr: "Créer une clé API", de: "API-Schlüssel erstellen", it: "Crea chiave API", pt: "Criar chave API", nl: "API-sleutel maken", pl: "Utwórz klucz API", ja: "APIキーを作成" },
"API key": { fr: "Clé API", de: "API-Schlüssel", it: "Chiave API", pt: "Chave API", nl: "API-sleutel", pl: "Klucz API", ja: "APIキー" },
"Key name": { fr: "Nom de la clé", de: "Schlüsselname", it: "Nome chiave", pt: "Nome da chave", nl: "Sleutelnaam", pl: "Nazwa klucza", ja: "キー名" },
"Store it somewhere safe.": { fr: "Conservez-la en lieu sûr.", de: "Bewahren Sie ihn sicher auf.", it: "Conservala in un posto sicuro.", pt: "Guarde-a num local seguro.", nl: "Bewaar hem op een veilige plek.", pl: "Przechowuj go w bezpiecznym miejscu.", ja: "安全な場所に保管してください。" },
"Last Used": { fr: "Dernière utilisation", de: "Zuletzt verwendet", it: "Ultimo utilizzo", pt: "Última utilização", nl: "Laatst gebruikt", pl: "Ostatnio użyty", ja: "最終使用" },
"Expires {date}": { fr: "Expire le {date}", de: "Läuft ab am {date}", it: "Scade il {date}", pt: "Expira a {date}", nl: "Verloopt op {date}", pl: "Wygasa {date}", ja: "{date} に期限切れ" },
"Invite for {email}.": { fr: "Invitation pour {email}.", de: "Einladung für {email}.", it: "Invito per {email}.", pt: "Convite para {email}.", nl: "Uitnodiging voor {email}.", pl: "Zaproszenie dla {email}.", ja: "{email} 宛の招待です。" },
"Joined / expires": { fr: "Inscription / expiration", de: "Beigetreten / läuft ab", it: "Iscrizione / scadenza", pt: "Adesão / expira", nl: "Toegetreden / verloopt", pl: "Dołączył / wygasa", ja: "参加 / 期限" },
"Make admin": { fr: "Rendre admin", de: "Zum Admin machen", it: "Rendi admin", pt: "Tornar admin", nl: "Admin maken", pl: "Uczyń adminem", ja: "管理者にする" },
"Make member": { fr: "Rendre membre", de: "Zum Mitglied machen", it: "Rendi membro", pt: "Tornar membro", nl: "Lid maken", pl: "Uczyń członkiem", ja: "メンバーにする" },
"Revoke invite": { fr: "Révoquer l'invitation", de: "Einladung widerrufen", it: "Revoca invito", pt: "Revogar convite", nl: "Uitnodiging intrekken", pl: "Unieważnij zaproszenie", ja: "招待を取り消す" },
"Member actions": { fr: "Actions du membre", de: "Mitgliederaktionen", it: "Azioni del membro", pt: "Ações do membro", nl: "Acties voor lid", pl: "Akcje członka", ja: "メンバーの操作" },
"No teammates yet": { fr: "Pas encore de coéquipiers", de: "Noch keine Teammitglieder", it: "Ancora nessun compagno di team", pt: "Ainda sem colegas", nl: "Nog geen teamleden", pl: "Brak jeszcze członków zespołu", ja: "まだチームメンバーがいません" },
"Share accept link": { fr: "Partager le lien d'acceptation", de: "Annahmelink teilen", it: "Condividi link di accettazione", pt: "Partilhar link de aceitação", nl: "Acceptatielink delen", pl: "Udostępnij link akceptacji", ja: "承認リンクを共有" },
"Accept link copied.": { fr: "Lien d'acceptation copié.", de: "Annahmelink kopiert.", it: "Link di accettazione copiato.", pt: "Link de aceitação copiado.", nl: "Acceptatielink gekopieerd.", pl: "Skopiowano link akceptacji.", ja: "承認リンクをコピーしました。" },
"Invite teammate": { fr: "Inviter un coéquipier", de: "Teammitglied einladen", it: "Invita un collega", pt: "Convidar colega", nl: "Teamlid uitnodigen", pl: "Zaproś członka zespołu", ja: "チームメイトを招待" },
"colleague@example.com": { fr: "collegue@exemple.com", de: "kollege@beispiel.com", it: "collega@esempio.com", pt: "colega@exemplo.com", nl: "collega@voorbeeld.com", pl: "kolega@przyklad.com", ja: "colleague@example.com" },
"Go to Billing": { fr: "Aller à la facturation", de: "Zur Abrechnung", it: "Vai alla fatturazione", pt: "Ir para Faturação", nl: "Naar facturering", pl: "Przejdź do rozliczeń", ja: "請求へ" },
"Dashboard actions": { fr: "Actions du tableau de bord", de: "Dashboard-Aktionen", it: "Azioni dashboard", pt: "Ações do painel", nl: "Dashboardacties", pl: "Akcje panelu", ja: "ダッシュボードの操作" },
"No catalog data yet": { fr: "Pas encore de données catalogue", de: "Noch keine Katalogdaten", it: "Ancora nessun dato di catalogo", pt: "Ainda sem dados de catálogo", nl: "Nog geen catalogusgegevens", pl: "Brak jeszcze danych katalogu", ja: "まだカタログデータがありません" },
"Connect feed anyway": { fr: "Connecter un flux quand même", de: "Feed trotzdem verbinden", it: "Collega comunque un feed", pt: "Ligar feed mesmo assim", nl: "Feed toch koppelen", pl: "Podłącz feed mimo to", ja: "それでもフィードを接続" },
"Import and map": { fr: "Importer et mapper", de: "Importieren und zuordnen", it: "Importa e mappa", pt: "Importar e mapear", nl: "Importeren en mappen", pl: "Importuj i mapuj", ja: "インポートとマップ" },
"Browse and process": { fr: "Parcourir et traiter", de: "Durchsuchen und verarbeiten", it: "Sfoglia ed elabora", pt: "Explorar e processar", nl: "Bladeren en verwerken", pl: "Przeglądaj i przetwarzaj", ja: "閲覧と処理" },
"Monitor tasks": { fr: "Surveiller les tâches", de: "Aufgaben überwachen", it: "Monitora le attività", pt: "Monitorizar tarefas", nl: "Taken monitoren", pl: "Monitoruj zadania", ja: "タスクを監視" },
"Templates & download": { fr: "Modèles et téléchargement", de: "Vorlagen & Download", it: "Modelli e download", pt: "Modelos e transferência", nl: "Sjablonen & download", pl: "Szablony i pobieranie", ja: "テンプレートとダウンロード" },
"Sync a sample": { fr: "Synchroniser un échantillon", de: "Stichprobe synchronisieren", it: "Sincronizza un campione", pt: "Sincronizar uma amostra", nl: "Een steekproef synchroniseren", pl: "Synchronizuj próbkę", ja: "サンプルを同期" },
"Map source fields": { fr: "Mapper les champs source", de: "Quellfelder zuordnen", it: "Mappa campi origine", pt: "Mapear campos de origem", nl: "Bronvelden mappen", pl: "Mapuj pola źródła", ja: "ソースフィールドをマップ" },
"Add or connect a source": { fr: "Ajouter ou connecter une source", de: "Quelle hinzufügen oder verbinden", it: "Aggiungi o collega un'origine", pt: "Adicionar ou ligar uma origem", nl: "Bron toevoegen of koppelen", pl: "Dodaj lub podłącz źródło", ja: "ソースを追加または接続" },
"Latest processing jobs": { fr: "Dernières tâches de traitement", de: "Neueste Verarbeitungsjobs", it: "Ultimi processi di elaborazione", pt: "Últimas tarefas de processamento", nl: "Laatste verwerkingsjobs", pl: "Najnowsze zadania przetwarzania", ja: "最新の処理ジョブ" },
"Credits running low": { fr: "Crédits bientôt épuisés", de: "Credits werden knapp", it: "Crediti in esaurimento", pt: "Créditos a esgotar-se", nl: "Credits raken op", pl: "Kończą się kredyty", ja: "クレジットが少なくなっています" },
"Product limit reached": { fr: "Limite de produits atteinte", de: "Produktlimit erreicht", it: "Limite prodotti raggiunto", pt: "Limite de produtos atingido", nl: "Productlimiet bereikt", pl: "Osiągnięto limit produktów", ja: "商品上限に達しました" },
"You're out of AI credits": { fr: "Vous n'avez plus de crédits IA", de: "Ihre KI-Credits sind aufgebraucht", it: "Hai esaurito i crediti IA", pt: "Ficou sem créditos de IA", nl: "Uw AI-credits zijn op", pl: "Skończyły Ci się kredyty AI", ja: "AIクレジットがなくなりました" },
"You're on the Free plan": { fr: "Vous êtes sur l'offre Free", de: "Sie nutzen den Free-Plan", it: "Sei sul piano Free", pt: "Está no plano Free", nl: "U zit op het Free-plan", pl: "Korzystasz z planu Free", ja: "Freeプランをご利用中です" },
"Trial · {plan}": { fr: "Essai · {plan}", de: "Testphase · {plan}", it: "Prova · {plan}", pt: "Teste · {plan}", nl: "Proef · {plan}", pl: "Okres próbny · {plan}", ja: "トライアル · {plan}" },
"Revoke this invitation?": { fr: "Révoquer cette invitation ?", de: "Diese Einladung widerrufen?", it: "Revocare questo invito?", pt: "Revogar este convite?", nl: "Deze uitnodiging intrekken?", pl: "Unieważnić to zaproszenie?", ja: "この招待を取り消しますか?" },
"Invitation revoked.": { fr: "Invitation révoquée.", de: "Einladung widerrufen.", it: "Invito revocato.", pt: "Convite revogado.", nl: "Uitnodiging ingetrokken.", pl: "Zaproszenie unieważnione.", ja: "招待を取り消しました。" },
"Could not send invite": { fr: "Impossible d'envoyer l'invitation", de: "Einladung konnte nicht gesendet werden", it: "Impossibile inviare l'invito", pt: "Não foi possível enviar o convite", nl: "Uitnodiging kon niet worden verzonden", pl: "Nie można wysłać zaproszenia", ja: "招待を送信できませんでした" },
"Could not revoke invite": { fr: "Impossible de révoquer l'invitation", de: "Einladung konnte nicht widerrufen werden", it: "Impossibile revocare l'invito", pt: "Não foi possível revogar o convite", nl: "Uitnodiging kon niet worden ingetrokken", pl: "Nie można unieważnić zaproszenia", ja: "招待を取り消せませんでした" },
"Could not remove user": { fr: "Impossible de retirer l'utilisateur", de: "Benutzer konnte nicht entfernt werden", it: "Impossibile rimuovere l'utente", pt: "Não foi possível remover o utilizador", nl: "Gebruiker kon niet worden verwijderd", pl: "Nie można usunąć użytkownika", ja: "ユーザーを削除できませんでした" },
"Could not update role": { fr: "Impossible de mettre à jour le rôle", de: "Rolle konnte nicht aktualisiert werden", it: "Impossibile aggiornare il ruolo", pt: "Não foi possível atualizar o papel", nl: "Rol kon niet worden bijgewerkt", pl: "Nie można zaktualizować roli", ja: "ロールを更新できませんでした" },
"Could not verify invite": { fr: "Impossible de vérifier l'invitation", de: "Einladung konnte nicht verifiziert werden", it: "Impossibile verificare l'invito", pt: "Não foi possível verificar o convite", nl: "Uitnodiging kon niet worden geverifieerd", pl: "Nie można zweryfikować zaproszenia", ja: "招待を確認できませんでした" },
"Could not accept invite": { fr: "Impossible d'accepter l'invitation", de: "Einladung konnte nicht angenommen werden", it: "Impossibile accettare l'invito", pt: "Não foi possível aceitar o convite", nl: "Uitnodiging kon niet worden geaccepteerd", pl: "Nie można zaakceptować zaproszenia", ja: "招待を承認できませんでした" },
"Could not set password": { fr: "Impossible de définir le mot de passe", de: "Passwort konnte nicht festgelegt werden", it: "Impossibile impostare la password", pt: "Não foi possível definir a palavra-passe", nl: "Wachtwoord kon niet worden ingesteld", pl: "Nie można ustawić hasła", ja: "パスワードを設定できませんでした" },
"Enter a valid email address.": { fr: "Saisissez une adresse e-mail valide.", de: "Geben Sie eine gültige E-Mail-Adresse ein.", it: "Inserisci un indirizzo email valido.", pt: "Introduza um endereço de e-mail válido.", nl: "Voer een geldig e-mailadres in.", pl: "Wprowadź prawidłowy adres e-mail.", ja: "有効なメールアドレスを入力してください。" },
"Companies need at least one admin": { fr: "Les entreprises ont besoin d'au moins un administrateur", de: "Unternehmen benötigen mindestens einen Admin", it: "Le aziende necessitano di almeno un amministratore", pt: "As empresas precisam de pelo menos um administrador", nl: "Bedrijven hebben minstens één beheerder nodig", pl: "Firmy potrzebują co najmniej jednego administratora", ja: "会社には少なくとも1人の管理者が必要です" },
"Set password first:": { fr: "Définissez d'abord le mot de passe :", de: "Zuerst Passwort festlegen:", it: "Imposta prima la password:", pt: "Defina primeiro a palavra-passe:", nl: "Stel eerst een wachtwoord in:", pl: "Najpierw ustaw hasło:", ja: "先にパスワードを設定:" },
"Platform admins can re-issue from": { fr: "Les administrateurs de la plateforme peuvent réémettre depuis", de: "Plattform-Admins können erneut ausstellen unter", it: "Gli amministratori della piattaforma possono riemettere da", pt: "Os administradores da plataforma podem reemitir a partir de", nl: "Platformbeheerders kunnen opnieuw uitgeven via", pl: "Administratorzy platformy mogą ponownie wystawić z", ja: "プラットフォーム管理者は次から再発行できます:" },
"Have a token? Open accept invite": { fr: "Vous avez un jeton ? Ouvrir accepter l'invitation", de: "Haben Sie ein Token? Einladung annehmen öffnen", it: "Hai un token? Apri accetta invito", pt: "Tem um token? Abrir aceitar convite", nl: "Heeft u een token? Open uitnodiging accepteren", pl: "Masz token? Otwórz akceptację zaproszenia", ja: "トークンがありますか?招待の承認を開く" },
"Have an invite or set-password link?": { fr: "Vous avez une invitation ou un lien de définition de mot de passe ?", de: "Haben Sie eine Einladung oder einen Passwort-Link?", it: "Hai un invito o un link per impostare la password?", pt: "Tem um convite ou um link para definir a palavra-passe?", nl: "Heeft u een uitnodiging of set-wachtwoordlink?", pl: "Masz zaproszenie lub link do ustawienia hasła?", ja: "招待またはパスワード設定リンクがありますか?" },
"Registers a company and its admin user.": { fr: "Enregistre une entreprise et son utilisateur administrateur.", de: "Registriert ein Unternehmen und dessen Admin-Benutzer.", it: "Registra un'azienda e il relativo utente amministratore.", pt: "Regista uma empresa e o respetivo utilizador administrador.", nl: "Registreert een bedrijf en de bijbehorende beheerdersgebruiker.", pl: "Rejestruje firmę i jej użytkownika administratora.", ja: "会社とその管理者ユーザーを登録します。" },
"Checking invite…": { fr: "Vérification de l'invitation…", de: "Einladung wird geprüft…", it: "Verifica invito…", pt: "A verificar convite…", nl: "Uitnodiging controleren…", pl: "Sprawdzanie zaproszenia…", ja: "招待を確認中…" },
"Invite token": { fr: "Jeton d'invitation", de: "Einladungs-Token", it: "Token di invito", pt: "Token de convite", nl: "Uitnodigingstoken", pl: "Token zaproszenia", ja: "招待トークン" },
"Reset token": { fr: "Jeton de réinitialisation", de: "Reset-Token", it: "Token di reimpostazione", pt: "Token de redefinição", nl: "Resettoken", pl: "Token resetowania", ja: "リセットトークン" },
"At least 8 characters. No other complexity rules.": { fr: "Au moins 8 caractères. Aucune autre règle de complexité.", de: "Mindestens 8 Zeichen. Keine weiteren Komplexitätsregeln.", it: "Almeno 8 caratteri. Nessun'altra regola di complessità.", pt: "Pelo menos 8 caracteres. Sem outras regras de complexidade.", nl: "Minimaal 8 tekens. Geen andere complexiteitsregels.", pl: "Co najmniej 8 znaków. Brak innych reguł złożoności.", ja: "8文字以上。その他の複雑さの規則はありません。" },
"Switch account:": { fr: "Changer de compte :", de: "Konto wechseln:", it: "Cambia account:", pt: "Mudar de conta:", nl: "Account wisselen:", pl: "Zmień konto:", ja: "アカウント切替:" },
"Re-issue path:": { fr: "Chemin de réémission :", de: "Neuausstellungs-Pfad:", it: "Percorso di riemissione:", pt: "Caminho de reemissão:", nl: "Pad voor opnieuw uitgeven:", pl: "Ścieżka ponownego wystawienia:", ja: "再発行の手順:" },
"Wrong account for this invite": { fr: "Mauvais compte pour cette invitation", de: "Falsches Konto für diese Einladung", it: "Account errato per questo invito", pt: "Conta errada para este convite", nl: "Verkeerd account voor deze uitnodiging", pl: "Złe konto dla tego zaproszenia", ja: "この招待には別のアカウントが必要です" },
"{email} removed.": { fr: "{email} retiré.", de: "{email} entfernt.", it: "{email} rimosso.", pt: "{email} removido.", nl: "{email} verwijderd.", pl: "Usunięto {email}.", ja: "{email} を削除しました。" },
"{email} is now {role}.": { fr: "{email} est maintenant {role}.", de: "{email} ist jetzt {role}.", it: "{email} ora è {role}.", pt: "{email} é agora {role}.", nl: "{email} is nu {role}.", pl: "{email} jest teraz {role}.", ja: "{email} は現在 {role} です。" },
"{action} {email} to {role}?": { fr: "{action} {email} en {role} ?", de: "{email} zu {role} {action}?", it: "{action} {email} a {role}?", pt: "{action} {email} para {role}?", nl: "{email} naar {role} {action}?", pl: "{action} {email} do {role}?", ja: "{email} を {role} に{action}しますか?" },
"Remove {email} from this company?": { fr: "Retirer {email} de cette entreprise ?", de: "{email} aus diesem Unternehmen entfernen?", it: "Rimuovere {email} da questa azienda?", pt: "Remover {email} desta empresa?", nl: "{email} uit dit bedrijf verwijderen?", pl: "Usunąć {email} z tej firmy?", ja: "{email} をこの会社から削除しますか?" },
"Live totals for {name}": { fr: "Totaux en direct pour {name}", de: "Live-Summen für {name}", it: "Totali in tempo reale per {name}", pt: "Totais em direto para {name}", nl: "Live totalen voor {name}", pl: "Bieżące sumy dla {name}", ja: "{name} のリアルタイム合計" },
"Trial ends {date}. {credits} credits remaining.": { fr: "L'essai se termine le {date}. {credits} crédits restants.", de: "Testphase endet am {date}. {credits} Credits übrig.", it: "La prova termina il {date}. {credits} crediti rimanenti.", pt: "O teste termina a {date}. {credits} créditos restantes.", nl: "Proef eindigt op {date}. {credits} credits resterend.", pl: "Okres próbny kończy się {date}. Pozostało {credits} kredytów.", ja: "トライアルは {date} に終了します。残りクレジット {credits}。" },
"{credits} credits remaining on your trial.": { fr: "{credits} crédits restants sur votre essai.", de: "{credits} Credits verbleiben in Ihrer Testphase.", it: "{credits} crediti rimanenti nella prova.", pt: "{credits} créditos restantes no seu teste.", nl: "{credits} credits resterend op uw proef.", pl: "Pozostało {credits} kredytów w okresie próbnym.", ja: "トライアルの残りクレジットは {credits} です。" },
"Feeds, products, jobs, and export.": { fr: "Flux, produits, tâches et export.", de: "Feeds, Produkte, Jobs und Export.", it: "Feed, prodotti, processi ed esportazione.", pt: "Feeds, produtos, tarefas e exportação.", nl: "Feeds, producten, jobs en export.", pl: "Feedy, produkty, zadania i eksport.", ja: "フィード、商品、ジョブ、エクスポート。" }
};
// Merge seed.es + MORE into full phrase-map
const map = { ...seed };
for (const [enText, langs] of Object.entries(MORE)) {
map[enText] = { ...(map[enText] || {}), ...langs };
}
// Also attach es from EXTRA_ES for any en phrases covered by keys
for (const [k, es] of Object.entries(EXTRA_ES.es)) {
const e = en[k];
if (!e) continue;
map[e] = { ...(map[e] || {}), es };
}
fs.writeFileSync("phrase-map.json", JSON.stringify(map, null, 2));
console.log("phrase-map entries", Object.keys(map).length);
// Count coverage vs needed phrases
const needed = JSON.parse(fs.readFileSync("_phrases-needed.json", "utf8"));
let covered = 0;
const gaps = [];
for (const p of needed) {
const m = map[p];
if (m && m.fr && m.de && m.it && m.pt && m.nl && m.pl && m.ja) covered++;
else gaps.push(p);
}
console.log("needed", needed.length, "fully covered", covered, "gaps", gaps.length);
fs.writeFileSync("_phrase-gaps.json", JSON.stringify(gaps, null, 2));
+4
View File
@@ -0,0 +1,4 @@
import fs from "node:fs";
import { EXTRA } from "./locale-extra-es.mjs";
fs.writeFileSync("_es-extra-keys.txt", Object.keys(EXTRA.es).join("\n"));
console.log(Object.keys(EXTRA.es).length);
+643
View File
@@ -0,0 +1,643 @@
import fs from "node:fs";
const map = JSON.parse(fs.readFileSync("phrase-map.json", "utf8"));
const GAPS = {
"This account still needs a password. Open your invite link, or ask an admin to re-issue one.": {
es: "Esta cuenta aún necesita una contraseña. Abre el enlace de invitación o pide a un administrador que emita uno nuevo.",
fr: "Ce compte a encore besoin d'un mot de passe. Ouvrez votre lien d'invitation, ou demandez à un administrateur d'en émettre un nouveau.",
de: "Dieses Konto benötigt noch ein Passwort. Öffnen Sie Ihren Einladungslink oder bitten Sie einen Admin, einen neuen auszustellen.",
it: "Questo account richiede ancora una password. Apri il link di invito o chiedi a un amministratore di generarne uno nuovo.",
pt: "Esta conta ainda precisa de uma palavra-passe. Abra o link do convite ou peça a um administrador para emitir um novo.",
nl: "Dit account heeft nog een wachtwoord nodig. Open uw uitnodigingslink of vraag een beheerder om een nieuwe.",
pl: "To konto nadal wymaga hasła. Otwórz link zaproszenia lub poproś administratora o wystawienie nowego.",
ja: "このアカウントにはまだパスワードが必要です。招待リンクを開くか、管理者に再発行を依頼してください。"
},
"use the invite link from your email. If the link went to an old address (email drift), ask a company admin to re-issue a set-password invite to {email}.": {
es: "usa el enlace de invitación de tu correo. Si el enlace fue a una dirección antigua (cambio de correo), pide a un administrador de la empresa que emita una nueva invitación para establecer contraseña a {email}.",
fr: "utilisez le lien d'invitation de votre e-mail. Si le lien a été envoyé à une ancienne adresse (dérive d'e-mail), demandez à un administrateur de l'entreprise de renvoyer une invitation de définition de mot de passe à {email}.",
de: "nutzen Sie den Einladungslink aus Ihrer E-Mail. Wenn der Link an eine alte Adresse ging (E-Mail-Drift), bitten Sie einen Unternehmens-Admin, eine neue Passwort-Einladung an {email} auszustellen.",
it: "usa il link di invito dalla tua email. Se il link è andato a un indirizzo vecchio (deriva email), chiedi a un amministratore dell'azienda di riemettere un invito per impostare la password a {email}.",
pt: "utilize o link do convite do seu e-mail. Se o link foi para um endereço antigo (desvio de e-mail), peça a um administrador da empresa para emitir um novo convite de definição de palavra-passe para {email}.",
nl: "gebruik de uitnodigingslink uit uw e-mail. Als de link naar een oud adres ging (e-maildrift), vraag dan een bedrijfsbeheerder om een nieuwe set-wachtwoorduitnodiging naar {email} te sturen.",
pl: "użyj linku zaproszenia z e-maila. Jeśli link poszedł na stary adres (dryf e-mail), poproś administratora firmy o ponowne wystawienie zaproszenia do ustawienia hasła na {email}.",
ja: "メールの招待リンクを使用してください。リンクが古いアドレスに送られた場合(メール変更)、会社の管理者に {email} 向けのパスワード設定招待の再発行を依頼してください。"
},
"Set your password to join the company. Your admin assigned either Member (day-to-day work) or Admin (team and billing).": {
es: "Establece tu contraseña para unirte a la empresa. Tu administrador asignó Miembro (trabajo diario) o Admin (equipo y facturación).",
fr: "Définissez votre mot de passe pour rejoindre l'entreprise. Votre administrateur a attribué Membre (travail quotidien) ou Admin (équipe et facturation).",
de: "Legen Sie Ihr Passwort fest, um dem Unternehmen beizutreten. Ihr Admin hat entweder Mitglied (Tagesgeschäft) oder Admin (Team und Abrechnung) zugewiesen.",
it: "Imposta la password per unirti all'azienda. Il tuo amministratore ha assegnato Membro (lavoro quotidiano) o Admin (team e fatturazione).",
pt: "Defina a sua palavra-passe para aderir à empresa. O seu administrador atribuiu Membro (trabalho diário) ou Admin (equipa e faturação).",
nl: "Stel uw wachtwoord in om toe te treden tot het bedrijf. Uw beheerder heeft Lid (dagelijks werk) of Admin (team en facturering) toegewezen.",
pl: "Ustaw hasło, aby dołączyć do firmy. Administrator przypisał rolę Członek (codzienna praca) lub Admin (zespół i rozliczenia).",
ja: "会社に参加するにはパスワードを設定してください。管理者はメンバー(日常業務)または管理者(チームと請求)のいずれかを割り当てています。"
},
"Choose a password for your migrated Descrybe account (at least 8 characters).": {
es: "Elige una contraseña para tu cuenta Descrybe migrada (al menos 8 caracteres).",
fr: "Choisissez un mot de passe pour votre compte Descrybe migré (au moins 8 caractères).",
de: "Wählen Sie ein Passwort für Ihr migriertes Descrybe-Konto (mindestens 8 Zeichen).",
it: "Scegli una password per il tuo account Descrybe migrato (almeno 8 caratteri).",
pt: "Escolha uma palavra-passe para a sua conta Descrybe migrada (pelo menos 8 caracteres).",
nl: "Kies een wachtwoord voor uw gemigreerde Descrybe-account (minimaal 8 tekens).",
pl: "Wybierz hasło do zmigrowanego konta Descrybe (co najmniej 8 znaków).",
ja: "移行されたDescrybeアカウント用のパスワードを選んでください(8文字以上)。"
},
"Invite link recognized. Enter a password below to continue — the secret is not shown on this page.": {
es: "Enlace de invitación reconocido. Introduce una contraseña abajo para continuar — el secreto no se muestra en esta página.",
fr: "Lien d'invitation reconnu. Saisissez un mot de passe ci-dessous pour continuer — le secret n'est pas affiché sur cette page.",
de: "Einladungslink erkannt. Geben Sie unten ein Passwort ein, um fortzufahren — das Geheimnis wird auf dieser Seite nicht angezeigt.",
it: "Link di invito riconosciuto. Inserisci una password qui sotto per continuare — il segreto non è mostrato in questa pagina.",
pt: "Link de convite reconhecido. Introduza uma palavra-passe abaixo para continuar — o segredo não é mostrado nesta página.",
nl: "Uitnodigingslink herkend. Voer hieronder een wachtwoord in om door te gaan — het geheim wordt op deze pagina niet getoond.",
pl: "Rozpoznano link zaproszenia. Wprowadź hasło poniżej, aby kontynuować — sekret nie jest wyświetlany na tej stronie.",
ja: "招待リンクを認識しました。続行するには下にパスワードを入力してください — このページにシークレットは表示されません。"
},
"Reset link recognized. Enter a password below to continue — the secret is not shown on this page.": {
es: "Enlace de restablecimiento reconocido. Introduce una contraseña abajo para continuar — el secreto no se muestra en esta página.",
fr: "Lien de réinitialisation reconnu. Saisissez un mot de passe ci-dessous pour continuer — le secret n'est pas affiché sur cette page.",
de: "Reset-Link erkannt. Geben Sie unten ein Passwort ein, um fortzufahren — das Geheimnis wird auf dieser Seite nicht angezeigt.",
it: "Link di reimpostazione riconosciuto. Inserisci una password qui sotto per continuare — il segreto non è mostrato in questa pagina.",
pt: "Link de redefinição reconhecido. Introduza uma palavra-passe abaixo para continuar — o segredo não é mostrado nesta página.",
nl: "Resetlink herkend. Voer hieronder een wachtwoord in om door te gaan — het geheim wordt op deze pagina niet getoond.",
pl: "Rozpoznano link resetowania. Wprowadź hasło poniżej, aby kontynuować — sekret nie jest wyświetlany na tej stronie.",
ja: "リセットリンクを認識しました。続行するには下にパスワードを入力してください — このページにシークレットは表示されません。"
},
"Paste the token from your invite email. It is masked in this field.": {
es: "Pega el token del correo de invitación. Se muestra enmascarado en este campo.",
fr: "Collez le jeton de votre e-mail d'invitation. Il est masqué dans ce champ.",
de: "Fügen Sie das Token aus Ihrer Einladungs-E-Mail ein. Es wird in diesem Feld maskiert angezeigt.",
it: "Incolla il token dall'email di invito. È mascherato in questo campo.",
pt: "Cole o token do e-mail de convite. É mascarado neste campo.",
nl: "Plak het token uit uw uitnodigingsmail. Het wordt in dit veld gemaskeerd.",
pl: "Wklej token z e-maila z zaproszeniem. Jest maskowany w tym polu.",
ja: "招待メールのトークンを貼り付けてください。このフィールドではマスク表示されます。"
},
"Could not verify set-password link": {
es: "No se pudo verificar el enlace para establecer contraseña",
fr: "Impossible de vérifier le lien de définition du mot de passe",
de: "Passwort-Link konnte nicht verifiziert werden",
it: "Impossibile verificare il link per impostare la password",
pt: "Não foi possível verificar o link de definição de palavra-passe",
nl: "Set-wachtwoordlink kon niet worden geverifieerd",
pl: "Nie można zweryfikować linku ustawienia hasła",
ja: "パスワード設定リンクを確認できませんでした"
},
"This invite is invalid or expired. Ask your company admin to send a new invite, then open the new link (or paste the new token below).": {
es: "Esta invitación no es válida o ha caducado. Pide a tu administrador de la empresa que envíe una nueva invitación y abre el nuevo enlace (o pega el nuevo token abajo).",
fr: "Cette invitation est invalide ou expirée. Demandez à votre administrateur d'envoyer une nouvelle invitation, puis ouvrez le nouveau lien (ou collez le nouveau jeton ci-dessous).",
de: "Diese Einladung ist ungültig oder abgelaufen. Bitten Sie Ihren Unternehmens-Admin um eine neue Einladung und öffnen Sie den neuen Link (oder fügen Sie das neue Token unten ein).",
it: "Questo invito non è valido o è scaduto. Chiedi all'amministratore dell'azienda di inviare un nuovo invito, poi apri il nuovo link (o incolla il nuovo token qui sotto).",
pt: "Este convite é inválido ou expirou. Peça ao administrador da empresa para enviar um novo convite e abra o novo link (ou cole o novo token abaixo).",
nl: "Deze uitnodiging is ongeldig of verlopen. Vraag uw bedrijfsbeheerder om een nieuwe uitnodiging te sturen en open de nieuwe link (of plak het nieuwe token hieronder).",
pl: "To zaproszenie jest nieprawidłowe lub wygasło. Poproś administratora firmy o nowe zaproszenie, a następnie otwórz nowy link (lub wklej nowy token poniżej).",
ja: "この招待は無効または期限切れです。会社の管理者に新しい招待の送信を依頼し、新しいリンクを開くか(下に新しいトークンを貼り付けてください)。"
},
"This set-password link is invalid or expired. Ask a company or platform admin to re-issue it, then open the new link (or paste the new token below).": {
es: "Este enlace para establecer contraseña no es válido o ha caducado. Pide a un administrador de la empresa o de la plataforma que lo reemita y abre el nuevo enlace (o pega el nuevo token abajo).",
fr: "Ce lien de définition de mot de passe est invalide ou expiré. Demandez à un administrateur de l'entreprise ou de la plateforme de le réémettre, puis ouvrez le nouveau lien (ou collez le nouveau jeton ci-dessous).",
de: "Dieser Passwort-Link ist ungültig oder abgelaufen. Bitten Sie einen Unternehmens- oder Plattform-Admin um Neuausstellung und öffnen Sie den neuen Link (oder fügen Sie das neue Token unten ein).",
it: "Questo link per impostare la password non è valido o è scaduto. Chiedi a un amministratore dell'azienda o della piattaforma di riemetterlo, poi apri il nuovo link (o incolla il nuovo token qui sotto).",
pt: "Este link de definição de palavra-passe é inválido ou expirou. Peça a um administrador da empresa ou da plataforma para o reemitir e abra o novo link (ou cole o novo token abaixo).",
nl: "Deze set-wachtwoordlink is ongeldig of verlopen. Vraag een bedrijfs- of platformbeheerder om hem opnieuw uit te geven en open de nieuwe link (of plak het nieuwe token hieronder).",
pl: "Ten link do ustawienia hasła jest nieprawidłowy lub wygasł. Poproś administratora firmy lub platformy o ponowne wystawienie, a następnie otwórz nowy link (lub wklej nowy token poniżej).",
ja: "このパスワード設定リンクは無効または期限切れです。会社またはプラットフォームの管理者に再発行を依頼し、新しいリンクを開くか(下に新しいトークンを貼り付けてください)。"
},
"You're signed in as a different email than this invite.": {
es: "Has iniciado sesión con un correo distinto al de esta invitación.",
fr: "Vous êtes connecté avec un e-mail différent de celui de cette invitation.",
de: "Sie sind mit einer anderen E-Mail angemeldet als diese Einladung.",
it: "Hai effettuato l'accesso con un'email diversa da questo invito.",
pt: "Tem sessão iniciada com um e-mail diferente deste convite.",
nl: "U bent ingelogd met een ander e-mailadres dan deze uitnodiging.",
pl: "Jesteś zalogowany na inny e-mail niż w tym zaproszeniu.",
ja: "この招待とは別のメールアドレスでログインしています。"
},
"Expired link? Ask an admin to re-issue — there is no self-serve resend API. Platform admins:": {
es: "¿Enlace caducado? Pide a un administrador que lo reemita — no hay API de reenvío autoservicio. Administradores de plataforma:",
fr: "Lien expiré ? Demandez à un administrateur de le réémettre — il n'y a pas d'API de renvoi en libre-service. Administrateurs de plateforme :",
de: "Abgelaufener Link? Bitten Sie einen Admin um Neuausstellung — es gibt keine Self-Service-API zum erneuten Senden. Plattform-Admins:",
it: "Link scaduto? Chiedi a un amministratore di riemetterlo — non c'è un'API di reinvio self-service. Amministratori della piattaforma:",
pt: "Link expirado? Peça a um administrador para o reemitir — não há API de reenvio self-service. Administradores da plataforma:",
nl: "Verlopen link? Vraag een beheerder om opnieuw uit te geven — er is geen self-service-API voor opnieuw verzenden. Platformbeheerders:",
pl: "Wygasły link? Poproś administratora o ponowne wystawienie — nie ma API samodzielnego ponownego wysyłania. Administratorzy platformy:",
ja: "期限切れのリンクですか?管理者に再発行を依頼してください — セルフサービスの再送信APIはありません。プラットフォーム管理者:"
},
"Your account is ready. Next, open the dashboard to work with feeds and products, or review company settings.": {
es: "Tu cuenta está lista. A continuación, abre el panel para trabajar con feeds y productos, o revisa la configuración de la empresa.",
fr: "Votre compte est prêt. Ensuite, ouvrez le tableau de bord pour travailler avec les flux et les produits, ou consultez les paramètres de l'entreprise.",
de: "Ihr Konto ist bereit. Öffnen Sie als Nächstes das Dashboard, um mit Feeds und Produkten zu arbeiten, oder prüfen Sie die Unternehmenseinstellungen.",
it: "Il tuo account è pronto. Apri la dashboard per lavorare con feed e prodotti, oppure rivedi le impostazioni dell'azienda.",
pt: "A sua conta está pronta. Em seguida, abra o painel para trabalhar com feeds e produtos, ou reveja as definições da empresa.",
nl: "Uw account is klaar. Open vervolgens het dashboard om met feeds en producten te werken, of bekijk de bedrijfsinstellingen.",
pl: "Twoje konto jest gotowe. Następnie otwórz panel, aby pracować z feedami i produktami, lub przejrzyj ustawienia firmy.",
ja: "アカウントの準備ができました。次にダッシュボードを開いてフィードや商品を扱うか、会社の設定を確認してください。"
},
"Sign in with your email and new password to open your workspace.": {
es: "Inicia sesión con tu correo y la nueva contraseña para abrir tu espacio de trabajo.",
fr: "Connectez-vous avec votre e-mail et le nouveau mot de passe pour ouvrir votre espace de travail.",
de: "Melden Sie sich mit Ihrer E-Mail und dem neuen Passwort an, um Ihren Arbeitsbereich zu öffnen.",
it: "Accedi con la tua email e la nuova password per aprire il tuo spazio di lavoro.",
pt: "Inicie sessão com o seu e-mail e a nova palavra-passe para abrir o seu espaço de trabalho.",
nl: "Log in met uw e-mailadres en nieuwe wachtwoord om uw werkruimte te openen.",
pl: "Zaloguj się e-mailem i nowym hasłem, aby otworzyć przestrzeń roboczą.",
ja: "メールと新しいパスワードでログインしてワークスペースを開いてください。"
},
"After sign-in you land in your workspace — the greenfield setup tour is skipped.": {
es: "Tras iniciar sesión llegas a tu espacio de trabajo — se omite el recorrido de configuración inicial.",
fr: "Après la connexion, vous arrivez dans votre espace de travail — le parcours de configuration initiale est ignoré.",
de: "Nach der Anmeldung landen Sie in Ihrem Arbeitsbereich — die Greenfield-Einrichtungstour wird übersprungen.",
it: "Dopo l'accesso arrivi nel tuo spazio di lavoro — il tour di configurazione iniziale viene saltato.",
pt: "Após o início de sessão chega ao seu espaço de trabalho — o tour de configuração inicial é ignorado.",
nl: "Na het inloggen komt u in uw werkruimte — de greenfield-instellingstour wordt overgeslagen.",
pl: "Po zalogowaniu trafiasz do przestrzeni roboczej — pomijana jest wycieczka po konfiguracji początkowej.",
ja: "ログイン後はワークスペースに入ります — 初期セットアップツアーはスキップされます。"
},
"This link is for a different email than the one you're signed in with. Sign out to continue as the invited user, or stay signed in and ask an admin to re-issue the invite.": {
es: "Este enlace es para un correo distinto al de la sesión actual. Cierra sesión para continuar como el usuario invitado, o permanece conectado y pide a un administrador que reemita la invitación.",
fr: "Ce lien est destiné à un e-mail différent de celui avec lequel vous êtes connecté. Déconnectez-vous pour continuer en tant qu'utilisateur invité, ou restez connecté et demandez à un administrateur de réémettre l'invitation.",
de: "Dieser Link gilt für eine andere E-Mail als die, mit der Sie angemeldet sind. Melden Sie sich ab, um als eingeladener Benutzer fortzufahren, oder bleiben Sie angemeldet und bitten Sie einen Admin um Neuausstellung der Einladung.",
it: "Questo link è per un'email diversa da quella con cui hai effettuato l'accesso. Esci per continuare come utente invitato, oppure resta connesso e chiedi a un amministratore di riemettere l'invito.",
pt: "Este link é para um e-mail diferente daquele com que tem sessão iniciada. Termine a sessão para continuar como o utilizador convidado, ou mantenha a sessão e peça a um administrador para reemitir o convite.",
nl: "Deze link is voor een ander e-mailadres dan waarmee u bent ingelogd. Log uit om door te gaan als de uitgenodigde gebruiker, of blijf ingelogd en vraag een beheerder om de uitnodiging opnieuw uit te geven.",
pl: "Ten link jest dla innego e-maila niż ten, na który jesteś zalogowany. Wyloguj się, aby kontynuować jako zaproszony użytkownik, albo pozostań zalogowany i poproś administratora o ponowne wystawienie zaproszenia.",
ja: "このリンクは、現在ログイン中のメールとは別のアドレス宛です。招待されたユーザーとして続行するにはログアウトするか、ログインしたまま管理者に招待の再発行を依頼してください。"
},
"Signed in as {session}, but this invite is for {invite}.": {
es: "Sesión iniciada como {session}, pero esta invitación es para {invite}.",
fr: "Connecté en tant que {session}, mais cette invitation est pour {invite}.",
de: "Angemeldet als {session}, aber diese Einladung ist für {invite}.",
it: "Accesso come {session}, ma questo invito è per {invite}.",
pt: "Sessão iniciada como {session}, mas este convite é para {invite}.",
nl: "Ingelogd als {session}, maar deze uitnodiging is voor {invite}.",
pl: "Zalogowano jako {session}, ale to zaproszenie jest dla {invite}.",
ja: "{session} でログイン中ですが、この招待は {invite} 宛です。"
},
"Signed-in email does not match this invite.": {
es: "El correo de la sesión no coincide con esta invitación.",
fr: "L'e-mail de la session ne correspond pas à cette invitation.",
de: "Die angemeldete E-Mail stimmt nicht mit dieser Einladung überein.",
it: "L'email della sessione non corrisponde a questo invito.",
pt: "O e-mail da sessão não corresponde a este convite.",
nl: "Het ingelogde e-mailadres komt niet overeen met deze uitnodiging.",
pl: "E-mail sesji nie pasuje do tego zaproszenia.",
ja: "ログイン中のメールがこの招待と一致しません。"
},
"sign out, then finish this form with the invited email{emailSuffix}.": {
es: "cierra sesión y completa este formulario con el correo invitado{emailSuffix}.",
fr: "déconnectez-vous, puis terminez ce formulaire avec l'e-mail invité{emailSuffix}.",
de: "melden Sie sich ab und schließen Sie dieses Formular mit der eingeladenen E-Mail{emailSuffix} ab.",
it: "esci, poi completa questo modulo con l'email invitata{emailSuffix}.",
pt: "termine a sessão e conclua este formulário com o e-mail convidado{emailSuffix}.",
nl: "log uit en voltooi dit formulier met het uitgenodigde e-mailadres{emailSuffix}.",
pl: "wyloguj się, a następnie dokończ ten formularz zaproszonym e-mailem{emailSuffix}.",
ja: "ログアウトしてから、招待されたメール{emailSuffix}でこのフォームを完了してください。"
},
"if your real login email changed (email drift), ask a company admin to revoke this invite and send a new one to the email you use to sign in. Platform admins can also re-issue set-password links from Admin → Users.": {
es: "si cambió tu correo real de acceso (desfase de correo), pide a un administrador de la empresa que revoque esta invitación y envíe una nueva al correo con el que inicias sesión. Los administradores de plataforma también pueden reemitir enlaces para establecer contraseña desde Admin → Usuarios.",
fr: "si votre vrai e-mail de connexion a changé (dérive d'e-mail), demandez à un administrateur de l'entreprise de révoquer cette invitation et d'en envoyer une nouvelle à l'e-mail que vous utilisez pour vous connecter. Les administrateurs de la plateforme peuvent aussi réémettre des liens de définition de mot de passe depuis Admin → Utilisateurs.",
de: "wenn sich Ihre echte Anmelde-E-Mail geändert hat (E-Mail-Drift), bitten Sie einen Unternehmens-Admin, diese Einladung zu widerrufen und eine neue an die E-Mail zu senden, mit der Sie sich anmelden. Plattform-Admins können Passwort-Links auch unter Admin → Benutzer erneut ausstellen.",
it: "se la tua email di accesso reale è cambiata (deriva email), chiedi a un amministratore dell'azienda di revocare questo invito e inviarne uno nuovo all'email con cui accedi. Gli amministratori della piattaforma possono anche riemettere link per impostare la password da Admin → Utenti.",
pt: "se o seu e-mail real de início de sessão mudou (desvio de e-mail), peça a um administrador da empresa para revogar este convite e enviar um novo para o e-mail que utiliza para iniciar sessão. Os administradores da plataforma também podem reemitir links de definição de palavra-passe em Admin → Utilizadores.",
nl: "als uw echte login-e-mail is gewijzigd (e-maildrift), vraag dan een bedrijfsbeheerder om deze uitnodiging in te trekken en een nieuwe te sturen naar het e-mailadres waarmee u inlogt. Platformbeheerders kunnen ook set-wachtwoordlinks opnieuw uitgeven via Admin → Gebruikers.",
pl: "jeśli zmienił się Twój prawdziwy e-mail logowania (dryf e-mail), poproś administratora firmy o unieważnienie tego zaproszenia i wysłanie nowego na e-mail używany do logowania. Administratorzy platformy mogą też ponownie wystawiać linki ustawienia hasła w Admin → Użytkownicy.",
ja: "実際のログイン用メールが変わった場合(メール変更)、会社の管理者にこの招待の取り消しと、ログインに使うメールへの新しい招待送信を依頼してください。プラットフォーム管理者は「管理 → ユーザー」からパスワード設定リンクも再発行できます。"
},
"You don't have permission to open company settings. Ask a company admin for help.": {
es: "No tienes permiso para abrir la configuración de la empresa. Pide ayuda a un administrador de la empresa.",
fr: "Vous n'avez pas l'autorisation d'ouvrir les paramètres de l'entreprise. Demandez de l'aide à un administrateur.",
de: "Sie haben keine Berechtigung, die Unternehmenseinstellungen zu öffnen. Bitten Sie einen Unternehmens-Admin um Hilfe.",
it: "Non hai l'autorizzazione per aprire le impostazioni dell'azienda. Chiedi aiuto a un amministratore.",
pt: "Não tem permissão para abrir as definições da empresa. Peça ajuda a um administrador da empresa.",
nl: "U hebt geen toestemming om bedrijfsinstellingen te openen. Vraag een bedrijfsbeheerder om hulp.",
pl: "Nie masz uprawnień do otwarcia ustawień firmy. Poproś administratora firmy o pomoc.",
ja: "会社の設定を開く権限がありません。会社の管理者に問い合わせてください。"
},
"Only company admins can invite, promote, demote, or remove teammates.": {
es: "Solo los administradores de la empresa pueden invitar, ascender, degradar o eliminar compañeros.",
fr: "Seuls les administrateurs de l'entreprise peuvent inviter, promouvoir, rétrograder ou retirer des coéquipiers.",
de: "Nur Unternehmens-Admins können Teammitglieder einladen, befördern, herabstufen oder entfernen.",
it: "Solo gli amministratori dell'azienda possono invitare, promuovere, degradare o rimuovere compagni di team.",
pt: "Apenas administradores da empresa podem convidar, promover, despromover ou remover colegas.",
nl: "Alleen bedrijfsbeheerders kunnen teamleden uitnodigen, promoveren, degraderen of verwijderen.",
pl: "Tylko administratorzy firmy mogą zapraszać, awansować, degradować lub usuwać członków zespołu.",
ja: "同僚の招待、昇格、降格、削除ができるのは会社の管理者のみです。"
},
"Outbound email is not configured. Copy this one-time link and send it to the invitee. They'll set a password (at least 8 characters) and join with the role you chose.": {
es: "El correo saliente no está configurado. Copia este enlace de un solo uso y envíaselo al invitado. Establecerá una contraseña (al menos 8 caracteres) y se unirá con el rol que elegiste.",
fr: "L'e-mail sortant n'est pas configuré. Copiez ce lien à usage unique et envoyez-le à l'invité. Il définira un mot de passe (au moins 8 caractères) et rejoindra avec le rôle que vous avez choisi.",
de: "Ausgehende E-Mail ist nicht konfiguriert. Kopieren Sie diesen Einmal-Link und senden Sie ihn an den Eingeladenen. Er legt ein Passwort fest (mindestens 8 Zeichen) und tritt mit der von Ihnen gewählten Rolle bei.",
it: "L'email in uscita non è configurata. Copia questo link monouso e invialo all'invitato. Imposterà una password (almeno 8 caratteri) e si unirà con il ruolo che hai scelto.",
pt: "O e-mail de saída não está configurado. Copie este link de utilização única e envie-o ao convidado. Definirá uma palavra-passe (pelo menos 8 caracteres) e aderirá com o papel que escolheu.",
nl: "Uitgaande e-mail is niet geconfigureerd. Kopieer deze eenmalige link en stuur hem naar de genodigde. Die stelt een wachtwoord in (minimaal 8 tekens) en treedt toe met de rol die u koos.",
pl: "Wychodzący e-mail nie jest skonfigurowany. Skopiuj ten jednorazowy link i wyślij go zaproszonemu. Ustawi hasło (co najmniej 8 znaków) i dołączy z wybraną przez Ciebie rolą.",
ja: "送信メールが設定されていません。この一回限りのリンクをコピーして招待者に送ってください。パスワード(8文字以上)を設定し、選択したロールで参加します。"
},
"One-time accept invite link": {
es: "Enlace de aceptación de invitación de un solo uso",
fr: "Lien d'acceptation d'invitation à usage unique",
de: "Einmaliger Einladungs-Annahmelink",
it: "Link monouso di accettazione invito",
pt: "Link de aceitação de convite de utilização única",
nl: "Eenmalige acceptatie-uitnodigingslink",
pl: "Jednorazowy link akceptacji zaproszenia",
ja: "一回限りの招待承認リンク"
},
"You don't have permission to view the team list. Ask a company admin for help.": {
es: "No tienes permiso para ver la lista del equipo. Pide ayuda a un administrador de la empresa.",
fr: "Vous n'avez pas l'autorisation de voir la liste de l'équipe. Demandez de l'aide à un administrateur.",
de: "Sie haben keine Berechtigung, die Teamliste anzuzeigen. Bitten Sie einen Unternehmens-Admin um Hilfe.",
it: "Non hai l'autorizzazione per visualizzare l'elenco del team. Chiedi aiuto a un amministratore.",
pt: "Não tem permissão para ver a lista da equipa. Peça ajuda a um administrador da empresa.",
nl: "U hebt geen toestemming om de teamlijst te bekijken. Vraag een bedrijfsbeheerder om hulp.",
pl: "Nie masz uprawnień do przeglądania listy zespołu. Poproś administratora firmy o pomoc.",
ja: "チーム一覧を表示する権限がありません。会社の管理者に問い合わせてください。"
},
"Invite colleagues as Member (products and feeds) or Admin (team and company settings). Pending invites show here until accepted.": {
es: "Invita a colegas como Miembro (productos y feeds) o Admin (equipo y configuración de la empresa). Las invitaciones pendientes aparecen aquí hasta que se acepten.",
fr: "Invitez des collègues en tant que Membre (produits et flux) ou Admin (équipe et paramètres de l'entreprise). Les invitations en attente s'affichent ici jusqu'à acceptation.",
de: "Laden Sie Kollegen als Mitglied (Produkte und Feeds) oder Admin (Team und Unternehmenseinstellungen) ein. Ausstehende Einladungen erscheinen hier bis zur Annahme.",
it: "Invita colleghi come Membro (prodotti e feed) o Admin (team e impostazioni azienda). Gli inviti in sospeso compaiono qui fino all'accettazione.",
pt: "Convide colegas como Membro (produtos e feeds) ou Admin (equipa e definições da empresa). Os convites pendentes aparecem aqui até serem aceites.",
nl: "Nodig collega's uit als Lid (producten en feeds) of Admin (team en bedrijfsinstellingen). Openstaande uitnodigingen verschijnen hier tot ze zijn geaccepteerd.",
pl: "Zapraszaj współpracowników jako Członek (produkty i feedy) lub Admin (zespół i ustawienia firmy). Oczekujące zaproszenia są tu widoczne do akceptacji.",
ja: "同僚をメンバー(商品とフィード)または管理者(チームと会社設定)として招待します。未承認の招待はここに表示されます。"
},
"No teammates listed yet. Ask a company admin to send invites.": {
es: "Aún no hay compañeros en la lista. Pide a un administrador de la empresa que envíe invitaciones.",
fr: "Aucun coéquipier listé pour le moment. Demandez à un administrateur d'envoyer des invitations.",
de: "Noch keine Teammitglieder aufgelistet. Bitten Sie einen Unternehmens-Admin, Einladungen zu senden.",
it: "Ancora nessun compagno di team elencato. Chiedi a un amministratore di inviare inviti.",
pt: "Ainda não há colegas listados. Peça a um administrador da empresa para enviar convites.",
nl: "Nog geen teamleden weergegeven. Vraag een bedrijfsbeheerder om uitnodigingen te sturen.",
pl: "Brak jeszcze członków zespołu na liście. Poproś administratora firmy o wysłanie zaproszeń.",
ja: "まだチームメンバーが一覧にありません。会社の管理者に招待の送信を依頼してください。"
},
"Invite created for {email} as {role}. Copy the accept link below and share it — outbound email is not configured.": {
es: "Invitación creada para {email} como {role}. Copia el enlace de aceptación abajo y compártelo — el correo saliente no está configurado.",
fr: "Invitation créée pour {email} en tant que {role}. Copiez le lien d'acceptation ci-dessous et partagez-le — l'e-mail sortant n'est pas configuré.",
de: "Einladung für {email} als {role} erstellt. Kopieren Sie den Annahmelink unten und teilen Sie ihn — ausgehende E-Mail ist nicht konfiguriert.",
it: "Invito creato per {email} come {role}. Copia il link di accettazione qui sotto e condividilo — l'email in uscita non è configurata.",
pt: "Convite criado para {email} como {role}. Copie o link de aceitação abaixo e partilhe-o — o e-mail de saída não está configurado.",
nl: "Uitnodiging aangemaakt voor {email} als {role}. Kopieer de acceptatielink hieronder en deel hem — uitgaande e-mail is niet geconfigureerd.",
pl: "Utworzono zaproszenie dla {email} jako {role}. Skopiuj link akceptacji poniżej i udostępnij go — wychodzący e-mail nie jest skonfigurowany.",
ja: "{email} を {role} として招待を作成しました。下の承認リンクをコピーして共有してください — 送信メールは設定されていません。"
},
"Invite sent to {email} as {role}. They should open the email and accept before it expires.": {
es: "Invitación enviada a {email} como {role}. Debe abrir el correo y aceptar antes de que caduque.",
fr: "Invitation envoyée à {email} en tant que {role}. La personne doit ouvrir l'e-mail et accepter avant expiration.",
de: "Einladung an {email} als {role} gesendet. Die Person sollte die E-Mail öffnen und vor Ablauf annehmen.",
it: "Invito inviato a {email} come {role}. Deve aprire l'email e accettare prima della scadenza.",
pt: "Convite enviado para {email} como {role}. Deve abrir o e-mail e aceitar antes de expirar.",
nl: "Uitnodiging verzonden naar {email} als {role}. Die moet de e-mail openen en accepteren vóór de vervaldatum.",
pl: "Wysłano zaproszenie do {email} jako {role}. Osoba powinna otworzyć e-mail i zaakceptować przed wygaśnięciem.",
ja: "{email} に {role} として招待を送信しました。期限前にメールを開いて承認してください。"
},
"They'll get a link to set a password (at least 8 characters) and join this company.": {
es: "Recibirá un enlace para establecer una contraseña (al menos 8 caracteres) y unirse a esta empresa.",
fr: "Ils recevront un lien pour définir un mot de passe (au moins 8 caractères) et rejoindre cette entreprise.",
de: "Sie erhalten einen Link, um ein Passwort festzulegen (mindestens 8 Zeichen) und diesem Unternehmen beizutreten.",
it: "Riceveranno un link per impostare una password (almeno 8 caratteri) e unirsi a questa azienda.",
pt: "Receberão um link para definir uma palavra-passe (pelo menos 8 caracteres) e aderir a esta empresa.",
nl: "Ze krijgen een link om een wachtwoord in te stellen (minimaal 8 tekens) en toe te treden tot dit bedrijf.",
pl: "Otrzymają link do ustawienia hasła (co najmniej 8 znaków) i dołączenia do tej firmy.",
ja: "パスワード(8文字以上)を設定してこの会社に参加するためのリンクが届きます。"
},
"Members manage products and feeds. Admins can also invite teammates and change company settings.": {
es: "Los miembros gestionan productos y feeds. Los administradores también pueden invitar compañeros y cambiar la configuración de la empresa.",
fr: "Les membres gèrent les produits et les flux. Les administrateurs peuvent aussi inviter des coéquipiers et modifier les paramètres de l'entreprise.",
de: "Mitglieder verwalten Produkte und Feeds. Admins können auch Teammitglieder einladen und Unternehmenseinstellungen ändern.",
it: "I membri gestiscono prodotti e feed. Gli amministratori possono anche invitare colleghi e modificare le impostazioni dell'azienda.",
pt: "Os membros gerem produtos e feeds. Os administradores também podem convidar colegas e alterar as definições da empresa.",
nl: "Leden beheren producten en feeds. Beheerders kunnen ook teamleden uitnodigen en bedrijfsinstellingen wijzigen.",
pl: "Członkowie zarządzają produktami i feedami. Administratorzy mogą też zapraszać współpracowników i zmieniać ustawienia firmy.",
ja: "メンバーは商品とフィードを管理します。管理者は同僚の招待と会社設定の変更もできます。"
},
"Demo sandbox is empty — switch to A1 or connect a feed to see real catalog stats.": {
es: "La zona de pruebas demo está vacía — cambia a A1 o conecta un feed para ver estadísticas reales del catálogo.",
fr: "Le bac à sable démo est vide — basculez vers A1 ou connectez un flux pour voir de vraies stats catalogue.",
de: "Demo-Sandbox ist leer — wechseln Sie zu A1 oder verbinden Sie einen Feed, um echte Katalogstatistiken zu sehen.",
it: "La sandbox demo è vuota — passa ad A1 o collega un feed per vedere statistiche reali del catalogo.",
pt: "A sandbox de demonstração está vazia — mude para A1 ou ligue um feed para ver estatísticas reais do catálogo.",
nl: "Demo-sandbox is leeg — schakel over naar A1 of koppel een feed om echte catalogusstatistieken te zien.",
pl: "Piaskownica demo jest pusta — przełącz na A1 lub podłącz feed, aby zobaczyć realne statystyki katalogu.",
ja: "デモサンドボックスは空です — A1に切り替えるかフィードを接続して実際のカタログ統計を表示します。"
},
"Add a feed or upload a CSV to populate this workspace.": {
es: "Añade un feed o sube un CSV para poblar este espacio de trabajo.",
fr: "Ajoutez un flux ou téléversez un CSV pour remplir cet espace de travail.",
de: "Fügen Sie einen Feed hinzu oder laden Sie eine CSV hoch, um diesen Arbeitsbereich zu füllen.",
it: "Aggiungi un feed o carica un CSV per popolare questo spazio di lavoro.",
pt: "Adicione um feed ou carregue um CSV para preencher este espaço de trabalho.",
nl: "Voeg een feed toe of upload een CSV om deze werkruimte te vullen.",
pl: "Dodaj feed lub prześlij CSV, aby wypełnić tę przestrzeń roboczą.",
ja: "フィードを追加するかCSVをアップロードして、このワークスペースにデータを入れます。"
},
"Add a feed or upload a CSV to start using your credits.": {
es: "Añade un feed o sube un CSV para empezar a usar tus créditos.",
fr: "Ajoutez un flux ou téléversez un CSV pour commencer à utiliser vos crédits.",
de: "Fügen Sie einen Feed hinzu oder laden Sie eine CSV hoch, um Ihre Credits zu nutzen.",
it: "Aggiungi un feed o carica un CSV per iniziare a usare i tuoi crediti.",
pt: "Adicione um feed ou carregue um CSV para começar a usar os seus créditos.",
nl: "Voeg een feed toe of upload een CSV om uw credits te gebruiken.",
pl: "Dodaj feed lub prześlij CSV, aby zacząć używać kredytów.",
ja: "フィードを追加するかCSVをアップロードして、クレジットの利用を開始します。"
},
"Import → enrich → publish. Jump to the next step for {name}.": {
es: "Importar → enriquecer → publicar. Salta al siguiente paso para {name}.",
fr: "Importer → enrichir → publier. Passez à l'étape suivante pour {name}.",
de: "Importieren → anreichern → veröffentlichen. Zum nächsten Schritt für {name}.",
it: "Importa → arricchisci → pubblica. Vai al passo successivo per {name}.",
pt: "Importar → enriquecer → publicar. Salte para o passo seguinte para {name}.",
nl: "Importeren → verrijken → publiceren. Ga naar de volgende stap voor {name}.",
pl: "Importuj → wzbogacaj → publikuj. Przejdź do następnego kroku dla {name}.",
ja: "インポート → 強化 → 公開。{name} の次のステップへ。"
},
"Switch to A1 (or another seeded company) in the header, or connect a feed here to populate this sandbox.": {
es: "Cambia a A1 (u otra empresa con datos) en el encabezado, o conecta un feed aquí para poblar esta zona de pruebas.",
fr: "Basculez vers A1 (ou une autre entreprise seedée) dans l'en-tête, ou connectez un flux ici pour remplir ce bac à sable.",
de: "Wechseln Sie in der Kopfzeile zu A1 (oder einem anderen Seed-Unternehmen) oder verbinden Sie hier einen Feed, um diese Sandbox zu füllen.",
it: "Passa ad A1 (o un'altra azienda con dati) nell'intestazione, oppure collega un feed qui per popolare questa sandbox.",
pt: "Mude para A1 (ou outra empresa com dados) no cabeçalho, ou ligue um feed aqui para preencher esta sandbox.",
nl: "Schakel in de header over naar A1 (of een ander geseeded bedrijf), of koppel hier een feed om deze sandbox te vullen.",
pl: "Przełącz na A1 (lub inną firmę z danymi) w nagłówku albo podłącz tu feed, aby wypełnić tę piaskownicę.",
ja: "ヘッダーでA1(または別のシード済み会社)に切り替えるか、ここでフィードを接続してサンドボックスにデータを入れます。"
},
"Connect a feed or upload a CSV to start building your catalog.": {
es: "Conecta un feed o sube un CSV para empezar a crear tu catálogo.",
fr: "Connectez un flux ou téléversez un CSV pour commencer à construire votre catalogue.",
de: "Verbinden Sie einen Feed oder laden Sie eine CSV hoch, um Ihren Katalog aufzubauen.",
it: "Collega un feed o carica un CSV per iniziare a costruire il catalogo.",
pt: "Ligue um feed ou carregue um CSV para começar a criar o seu catálogo.",
nl: "Koppel een feed of upload een CSV om uw catalogus op te bouwen.",
pl: "Podłącz feed lub prześlij CSV, aby zacząć budować katalog.",
ja: "フィードを接続するかCSVをアップロードして、カタログの構築を開始します。"
},
"{used} of {max} products used. Feed mapping, basic cleanup, and EU energy labels (EPREL) are included; upgrade for AI titles and descriptions, and more capacity.": {
es: "{used} de {max} productos usados. El mapeo de feeds, la limpieza básica y las etiquetas energéticas de la UE (EPREL) están incluidos; actualiza para títulos y descripciones con IA, y más capacidad.",
fr: "{used} sur {max} produits utilisés. Le mapping des flux, le nettoyage de base et les labels énergétiques UE (EPREL) sont inclus ; passez à une offre supérieure pour les titres et descriptions IA, et plus de capacité.",
de: "{used} von {max} Produkten genutzt. Feed-Zuordnung, Basisbereinigung und EU-Energieetiketten (EPREL) sind enthalten; upgraden Sie für KI-Titel und -Beschreibungen sowie mehr Kapazität.",
it: "{used} di {max} prodotti usati. Mappatura feed, pulizia di base ed etichette energetiche UE (EPREL) sono inclusi; passa a un piano superiore per titoli e descrizioni IA e più capacità.",
pt: "{used} de {max} produtos usados. O mapeamento de feeds, a limpeza básica e as etiquetas energéticas da UE (EPREL) estão incluídos; atualize para títulos e descrições com IA e mais capacidade.",
nl: "{used} van {max} producten gebruikt. Feed-mapping, basisopschoning en EU-energielabels (EPREL) zijn inbegrepen; upgrade voor AI-titels en -beschrijvingen en meer capaciteit.",
pl: "Użyto {used} z {max} produktów. Mapowanie feedów, podstawowe czyszczenie i etykiety energetyczne UE (EPREL) są wliczone; ulepsz plan o tytuły i opisy AI oraz większą pojemność.",
ja: "{max} 件中 {used} 件の商品を使用中。フィードマッピング、基本クリーンアップ、EUエネルギーラベル(EPREL)は含まれます。AIタイトル・説明と容量増加はアップグレードが必要です。"
},
"Feed mapping, basic cleanup, and EU energy labels (EPREL) are included; upgrade for AI titles and descriptions, and more capacity.": {
es: "El mapeo de feeds, la limpieza básica y las etiquetas energéticas de la UE (EPREL) están incluidos; actualiza para títulos y descripciones con IA, y más capacidad.",
fr: "Le mapping des flux, le nettoyage de base et les labels énergétiques UE (EPREL) sont inclus ; passez à une offre supérieure pour les titres et descriptions IA, et plus de capacité.",
de: "Feed-Zuordnung, Basisbereinigung und EU-Energieetiketten (EPREL) sind enthalten; upgraden Sie für KI-Titel und -Beschreibungen sowie mehr Kapazität.",
it: "Mappatura feed, pulizia di base ed etichette energetiche UE (EPREL) sono inclusi; passa a un piano superiore per titoli e descrizioni IA e più capacità.",
pt: "O mapeamento de feeds, a limpeza básica e as etiquetas energéticas da UE (EPREL) estão incluídos; atualize para títulos e descrições com IA e mais capacidade.",
nl: "Feed-mapping, basisopschoning en EU-energielabels (EPREL) zijn inbegrepen; upgrade voor AI-titels en -beschrijvingen en meer capaciteit.",
pl: "Mapowanie feedów, podstawowe czyszczenie i etykiety energetyczne UE (EPREL) są wliczone; ulepsz plan o tytuły i opisy AI oraz większą pojemność.",
ja: "フィードマッピング、基本クリーンアップ、EUエネルギーラベル(EPREL)は含まれます。AIタイトル・説明と容量増加はアップグレードが必要です。"
},
"Buy more credits or upgrade your plan to keep processing.": {
es: "Compra más créditos o actualiza tu plan para seguir procesando.",
fr: "Achetez plus de crédits ou passez à une offre supérieure pour continuer le traitement.",
de: "Kaufen Sie mehr Credits oder upgraden Sie Ihren Plan, um die Verarbeitung fortzusetzen.",
it: "Acquista altri crediti o passa a un piano superiore per continuare l'elaborazione.",
pt: "Compre mais créditos ou atualize o plano para continuar a processar.",
nl: "Koop meer credits of upgrade uw plan om te blijven verwerken.",
pl: "Kup więcej kredytów lub ulepsz plan, aby kontynuować przetwarzanie.",
ja: "処理を続けるにはクレジットを追加購入するかプランをアップグレードしてください。"
},
"Your {plan} plan allows {max} products ({count} in catalog). Upgrade to process more.": {
es: "Tu plan {plan} permite {max} productos ({count} en el catálogo). Actualiza para procesar más.",
fr: "Votre offre {plan} autorise {max} produits ({count} dans le catalogue). Passez à une offre supérieure pour en traiter plus.",
de: "Ihr {plan}-Plan erlaubt {max} Produkte ({count} im Katalog). Upgraden Sie, um mehr zu verarbeiten.",
it: "Il piano {plan} consente {max} prodotti ({count} nel catalogo). Passa a un piano superiore per elaborarne di più.",
pt: "O seu plano {plan} permite {max} produtos ({count} no catálogo). Atualize para processar mais.",
nl: "Uw {plan}-plan staat {max} producten toe ({count} in catalogus). Upgrade om meer te verwerken.",
pl: "Twój plan {plan} pozwala na {max} produktów ({count} w katalogu). Ulepsz, aby przetwarzać więcej.",
ja: "{plan} プランでは商品 {max} 件までです(カタログ内 {count} 件)。さらに処理するにはアップグレードしてください。"
},
"Your {plan} plan product limit is reached. Upgrade to process more.": {
es: "Se alcanzó el límite de productos de tu plan {plan}. Actualiza para procesar más.",
fr: "La limite de produits de votre offre {plan} est atteinte. Passez à une offre supérieure pour en traiter plus.",
de: "Das Produktlimit Ihres {plan}-Plans ist erreicht. Upgraden Sie, um mehr zu verarbeiten.",
it: "È stato raggiunto il limite prodotti del piano {plan}. Passa a un piano superiore per elaborarne di più.",
pt: "O limite de produtos do plano {plan} foi atingido. Atualize para processar mais.",
nl: "De productlimiet van uw {plan}-plan is bereikt. Upgrade om meer te verwerken.",
pl: "Osiągnięto limit produktów planu {plan}. Ulepsz, aby przetwarzać więcej.",
ja: "{plan} プランの商品上限に達しました。さらに処理するにはアップグレードしてください。"
},
"{remaining} of {total} credits left. Top up or upgrade before jobs stall.": {
es: "Quedan {remaining} de {total} créditos. Recarga o actualiza antes de que se detengan los trabajos.",
fr: "Il reste {remaining} crédits sur {total}. Rechargez ou passez à une offre supérieure avant que les tâches ne s'arrêtent.",
de: "{remaining} von {total} Credits übrig. Laden Sie auf oder upgraden Sie, bevor Jobs stoppen.",
it: "Restano {remaining} di {total} crediti. Ricarica o passa a un piano superiore prima che i processi si fermino.",
pt: "Restam {remaining} de {total} créditos. Recarregue ou atualize antes de as tarefas pararem.",
nl: "{remaining} van {total} credits over. Vul aan of upgrade voordat jobs stilvallen.",
pl: "Pozostało {remaining} z {total} kredytów. Doładuj lub ulepsz, zanim zadania się zatrzymają.",
ja: "クレジット残り {total} 中 {remaining}。ジョブが止まる前に補充またはアップグレードしてください。"
},
"No jobs yet — import products first.": {
es: "Aún no hay trabajos — importa productos primero.",
fr: "Pas encore de tâches — importez d'abord des produits.",
de: "Noch keine Jobs — importieren Sie zuerst Produkte.",
it: "Ancora nessun processo — importa prima i prodotti.",
pt: "Ainda sem tarefas — importe produtos primeiro.",
nl: "Nog geen jobs — importeer eerst producten.",
pl: "Brak jeszcze zadań — najpierw zaimportuj produkty.",
ja: "まだジョブがありません — 先に商品をインポートしてください。"
},
"No recent jobs. Start one from Products when you are ready.": {
es: "No hay trabajos recientes. Inicia uno desde Productos cuando estés listo.",
fr: "Aucune tâche récente. Démarrez-en une depuis Produits quand vous êtes prêt.",
de: "Keine aktuellen Jobs. Starten Sie einen unter Produkte, wenn Sie bereit sind.",
it: "Nessun processo recente. Avviane uno da Prodotti quando sei pronto.",
pt: "Sem tarefas recentes. Inicie uma em Produtos quando estiver pronto.",
nl: "Geen recente jobs. Start er een vanuit Producten wanneer u klaar bent.",
pl: "Brak ostatnich zadań. Uruchom jedno w Produktach, gdy będziesz gotowy.",
ja: "最近のジョブはありません。準備ができたら商品から開始してください。"
},
"Turn on the standard product columns Descrybe maps and processes.": {
es: "Activa las columnas de producto estándar que Descrybe mapea y procesa.",
fr: "Activez les colonnes produit standard que Descrybe mappe et traite.",
de: "Aktivieren Sie die Standard-Produktspalten, die Descrybe zuordnet und verarbeitet.",
it: "Attiva le colonne prodotto standard che Descrybe mappa ed elabora.",
pt: "Ative as colunas de produto padrão que o Descrybe mapeia e processa.",
nl: "Schakel de standaard productkolommen in die Descrybe mapt en verwerkt.",
pl: "Włącz standardowe kolumny produktów, które Descrybe mapuje i przetwarza.",
ja: "Descrybeがマップおよび処理する標準の商品列を有効にします。"
},
"Add a CSV/XML feed or connect a store so products can flow in.": {
es: "Añade un feed CSV/XML o conecta una tienda para que entren productos.",
fr: "Ajoutez un flux CSV/XML ou connectez une boutique pour faire entrer les produits.",
de: "Fügen Sie einen CSV/XML-Feed hinzu oder verbinden Sie einen Shop, damit Produkte einfließen können.",
it: "Aggiungi un feed CSV/XML o collega un negozio così che i prodotti possano entrare.",
pt: "Adicione um feed CSV/XML ou ligue uma loja para os produtos poderem entrar.",
nl: "Voeg een CSV/XML-feed toe of koppel een winkel zodat producten kunnen binnenkomen.",
pl: "Dodaj feed CSV/XML lub podłącz sklep, aby produkty mogły napływać.",
ja: "CSV/XMLフィードを追加するかストアを接続して、商品を取り込めるようにします。"
},
"Match supplier columns to Descrybe fields, then save the mapping.": {
es: "Asocia las columnas del proveedor a los campos de Descrybe y guarda el mapeo.",
fr: "Faites correspondre les colonnes fournisseur aux champs Descrybe, puis enregistrez le mapping.",
de: "Ordnen Sie Lieferantenspalten den Descrybe-Feldern zu und speichern Sie die Zuordnung.",
it: "Abbina le colonne del fornitore ai campi Descrybe, poi salva la mappatura.",
pt: "Faça corresponder as colunas do fornecedor aos campos Descrybe e guarde o mapeamento.",
nl: "Koppel leverancierskolommen aan Descrybe-velden en sla de mapping op.",
pl: "Dopasuj kolumny dostawcy do pól Descrybe, a następnie zapisz mapowanie.",
ja: "仕入先の列をDescrybeのフィールドに対応付け、マッピングを保存します。"
},
"Pull a small sample so you can verify mapping before a full run.": {
es: "Extrae una muestra pequeña para verificar el mapeo antes de una ejecución completa.",
fr: "Récupérez un petit échantillon pour vérifier le mapping avant une exécution complète.",
de: "Ziehen Sie eine kleine Stichprobe, um die Zuordnung vor einem vollständigen Lauf zu prüfen.",
it: "Recupera un piccolo campione per verificare la mappatura prima di un'esecuzione completa.",
pt: "Obtenha uma pequena amostra para verificar o mapeamento antes de uma execução completa.",
nl: "Haal een kleine steekproef op om de mapping te controleren vóór een volledige run.",
pl: "Pobierz małą próbkę, aby zweryfikować mapowanie przed pełnym uruchomieniem.",
ja: "本番実行の前にマッピングを確認できるよう、小さなサンプルを取得します。"
},
"Run processing on synced products to generate cleaned catalog content.": {
es: "Ejecuta el procesamiento sobre productos sincronizados para generar contenido de catálogo limpio.",
fr: "Lancez le traitement sur les produits synchronisés pour générer un contenu catalogue nettoyé.",
de: "Führen Sie die Verarbeitung für synchronisierte Produkte aus, um bereinigte Kataloginhalte zu erzeugen.",
it: "Esegui l'elaborazione sui prodotti sincronizzati per generare contenuti di catalogo puliti.",
pt: "Execute o processamento nos produtos sincronizados para gerar conteúdo de catálogo limpo.",
nl: "Voer verwerking uit op gesynchroniseerde producten om schone catalogusinhoud te genereren.",
pl: "Uruchom przetwarzanie zsynchronizowanych produktów, aby wygenerować oczyszczoną treść katalogu.",
ja: "同期済み商品を処理して、整備されたカタログコンテンツを生成します。"
},
"Create an export feed to publish cleaned products as XML or CSV.": {
es: "Crea un feed de exportación para publicar productos limpios como XML o CSV.",
fr: "Créez un flux d'export pour publier les produits nettoyés en XML ou CSV.",
de: "Erstellen Sie einen Export-Feed, um bereinigte Produkte als XML oder CSV zu veröffentlichen.",
it: "Crea un feed di esportazione per pubblicare prodotti puliti come XML o CSV.",
pt: "Crie um feed de exportação para publicar produtos limpos como XML ou CSV.",
nl: "Maak een exportfeed om schone producten als XML of CSV te publiceren.",
pl: "Utwórz feed eksportu, aby publikować oczyszczone produkty jako XML lub CSV.",
ja: "整備された商品をXMLまたはCSVとして公開するエクスポートフィードを作成します。"
},
"Company Settings": {
es: "Configuración de la empresa",
fr: "Paramètres de l'entreprise",
de: "Unternehmenseinstellungen",
it: "Impostazioni azienda",
pt: "Definições da empresa",
nl: "Bedrijfsinstellingen",
pl: "Ustawienia firmy",
ja: "会社の設定"
},
"Active company": { es: "Empresa activa" },
"Credits overview": { es: "Resumen de créditos" },
Plan: { es: "Plan" },
Used: { es: "Usados" },
"Company Information": { es: "Información de la empresa" },
"Update your company details": { es: "Actualiza los datos de tu empresa" },
"Company Name": { es: "Nombre de la empresa" },
"Your company name": { es: "Nombre de tu empresa" },
"Content Settings": { es: "Configuración de contenido" },
"Merge products with the same GTIN": { es: "Fusionar productos con el mismo GTIN" },
"Email integration": { es: "Integración de correo" },
"AI integrations": { es: "Integraciones de IA" },
"Operator alerts": { es: "Alertas del operador" },
"In-app toasts": { es: "Toasts en la app" },
"Email alerts": { es: "Alertas por correo" },
"API Keys": { es: "Claves API" },
"Create API Key": { es: "Crear clave API" },
"Create API key": { es: "Crear clave API" },
"API key": { es: "Clave API" },
"Key name": { es: "Nombre de la clave" },
"Store it somewhere safe.": { es: "Guárdala en un lugar seguro." },
Name: { es: "Nombre" },
Key: { es: "Clave" },
"Last Used": { es: "Último uso" },
"Resume tutorial": { es: "Reanudar tutorial" },
"Restart tutorial": { es: "Reiniciar tutorial" },
"Process products": { es: "Procesar productos" },
"Open products": { es: "Abrir productos" },
"Welcome to {name}": { es: "Bienvenido a {name}" },
Trial: { es: "Prueba" },
"Dashboard actions": { es: "Acciones del panel" },
Processing: {
es: "Procesando",
fr: "Traitement",
de: "Verarbeitung",
it: "Elaborazione",
pt: "A processar",
nl: "Verwerken",
pl: "Przetwarzanie",
ja: "処理中"
},
Completed: {
es: "Completado",
fr: "Terminé",
de: "Abgeschlossen",
it: "Completato",
pt: "Concluído",
nl: "Voltooid",
pl: "Ukończono",
ja: "完了"
},
Failed: {
es: "Fallido",
fr: "Échoué",
de: "Fehlgeschlagen",
it: "Non riuscito",
pt: "Falhou",
nl: "Mislukt",
pl: "Niepowodzenie",
ja: "失敗"
},
Cancelled: {
es: "Cancelado",
fr: "Annulé",
de: "Abgebrochen",
it: "Annullato",
pt: "Cancelado",
nl: "Geannuleerd",
pl: "Anulowano",
ja: "キャンセル済み"
},
Exports: {
es: "Exportaciones",
fr: "Exports",
de: "Exporte",
it: "Esportazioni",
pt: "Exportações",
nl: "Exports",
pl: "Eksporty",
ja: "エクスポート"
},
Actions: {
es: "Acciones",
fr: "Actions",
de: "Aktionen",
it: "Azioni",
pt: "Ações",
nl: "Acties",
pl: "Akcje",
ja: "操作"
},
"All {count} products failed.": {
es: "Fallaron los {count} productos.",
fr: "Les {count} produits ont échoué.",
de: "Alle {count} Produkte sind fehlgeschlagen.",
it: "Tutti i {count} prodotti non sono riusciti.",
pt: "Todos os {count} produtos falharam.",
nl: "Alle {count} producten zijn mislukt.",
pl: "Wszystkie {count} produktów nie powiodło się.",
ja: "{count} 件すべての商品が失敗しました。"
},
"{count} products failed.": {
es: "Fallaron {count} productos.",
fr: "{count} produits ont échoué.",
de: "{count} Produkte sind fehlgeschlagen.",
it: "{count} prodotti non sono riusciti.",
pt: "{count} produtos falharam.",
nl: "{count} producten zijn mislukt.",
pl: "{count} produktów nie powiodło się.",
ja: "{count} 件の商品が失敗しました。"
}
};
for (const [en, langs] of Object.entries(GAPS)) {
map[en] = { ...(map[en] || {}), ...langs };
}
fs.writeFileSync("phrase-map.json", JSON.stringify(map, null, 2));
console.log("phrase-map entries", Object.keys(map).length);
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
/**
* Harvest EN→locale phrase map from already-translated message keys,
* then merge into phrase-map.json (does not overwrite non-empty existing locales).
*
* Run: node apps/web/scripts/harvest-phrase-map.mjs
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.resolve(__dirname, "../src/lib/i18n/messages");
const mapPath = path.join(__dirname, "phrase-map.json");
const LOCALES = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
function load(code) {
return parseMessageDict(fs.readFileSync(path.join(messagesDir, `${code}.ts`), "utf8"));
}
const en = load("en");
const packs = Object.fromEntries(LOCALES.map((c) => [c, load(c)]));
const map = fs.existsSync(mapPath) ? JSON.parse(fs.readFileSync(mapPath, "utf8")) : {};
let harvestedPhrases = 0;
let filledSlots = 0;
let newPhrases = 0;
for (const [key, enVal] of Object.entries(en)) {
if (!enVal || typeof enVal !== "string") continue;
const byLoc = {};
let good = 0;
for (const code of LOCALES) {
const v = packs[code][key];
if (typeof v === "string" && v.trim() && v !== enVal) {
byLoc[code] = v;
good += 1;
}
}
if (good < 4) continue;
const existing = map[enVal] ?? {};
let changed = false;
const next = { ...existing };
for (const code of LOCALES) {
if ((!next[code] || !String(next[code]).trim()) && byLoc[code]) {
next[code] = byLoc[code];
filledSlots += 1;
changed = true;
}
}
if (changed) {
if (!map[enVal]) newPhrases += 1;
map[enVal] = next;
harvestedPhrases += 1;
}
}
fs.writeFileSync(mapPath, JSON.stringify(map, null, "\t") + "\n", "utf8");
console.log({
phraseCount: Object.keys(map).length,
harvestedPhrases,
newPhrases,
filledSlots,
});
+23
View File
@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";
const root = path.resolve("apps/api/internal/httpapi");
const msgs = new Set();
function walk(dir) {
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) walk(p);
else if (ent.name.endsWith(".go")) {
const s = fs.readFileSync(p, "utf8");
for (const m of s.matchAll(/Error\(w,\s*http\.Status\w+,\s*"([^"]+)"/g)) {
msgs.add(m[1]);
}
for (const m of s.matchAll(/CodedError\(w,\s*http\.Status\w+,\s*"[^"]+",\s*"([^"]+)"/g)) {
msgs.add(m[1]);
}
}
}
}
walk(root);
console.log([...msgs].sort().join("\n"));
console.log("COUNT", msgs.size);
+32
View File
@@ -0,0 +1,32 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";
// Re-parse PACKS by evaluating gen script is hard; instead diff en vs es.
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.resolve(__dirname, "../src/lib/i18n/messages");
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
const en = parseMessageDict(fs.readFileSync(path.join(messagesDir, "en.ts"), "utf8"));
const es = parseMessageDict(fs.readFileSync(path.join(messagesDir, "es.ts"), "utf8"));
const missing = {};
for (const [k, v] of Object.entries(en)) {
if (es[k] === v) missing[k] = v; // likely untranslated (same as en) OR intentionally same
}
// Prefer keys not in our known translated set — dump all where es === en
fs.writeFileSync(path.join(__dirname, "_missing.json"), JSON.stringify(missing, null, 2), "utf8");
console.log(`candidates same-as-en: ${Object.keys(missing).length}`);
+33
View File
@@ -0,0 +1,33 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
const en = parseMessageDict(
fs.readFileSync(path.resolve(__dirname, "../src/lib/i18n/messages/en.ts"), "utf8")
);
const fr = parseMessageDict(
fs.readFileSync(path.resolve(__dirname, "../src/lib/i18n/messages/fr.ts"), "utf8")
);
const phrases = {};
for (const [k, v] of Object.entries(en)) {
if (fr[k] === v) phrases[v] = true;
}
const list = Object.keys(phrases);
fs.writeFileSync(path.join(__dirname, "_phrases-needed.json"), JSON.stringify(list, null, 2));
console.log("unique phrases needed", list.length);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
/**
* Chrome + tutorial overlays for gen-locale-packs / rebuild.
* Brand/loanwords stay in SAME_AS_EN (locale-extra.mjs).
* One-shot scratch `_chrome-tutorial-data.mjs` was removed; packs already
* contain those strings under src/lib/i18n/messages.
*/
/** @type {Record<string, Record<string, string>>} */
export const EXTRA = {};
File diff suppressed because it is too large Load Diff
+238
View File
@@ -0,0 +1,238 @@
/** Supplemental UI strings merged into gen-locale-packs.mjs (auth/settings/dashboard extras). */
export const EXTRA = {
es: {
"common.askAdmin": "Pregunta a un administrador de la empresa",
"common.you": "Tú",
"common.active": "Activo",
"common.pending": "Pendiente",
"common.email": "Correo electrónico",
"common.password": "Contraseña",
"common.name": "Tu nombre",
"common.viewPricing": "Ver precios",
"common.backToSignIn": "Volver a iniciar sesión",
"common.signingOut": "Cerrando sesión…",
"common.signOutAndContinue": "Cerrar sesión y continuar",
"common.staySignedIn": "Seguir conectado",
"nav.section.feeds": "Feeds",
"nav.section.marketing": "Marketing",
"nav.feeds": "Feeds",
"nav.seo": "SEO",
"nav.admin": "Admin",
"auth.login.title": "Iniciar sesión",
"auth.login.description": "Usa tu correo y contraseña de Descrybe.",
"auth.login.submit": "Iniciar sesión",
"auth.login.submitting": "Iniciando sesión…",
"auth.login.failed": "Error al iniciar sesión",
"auth.login.passwordNotSet":
"Esta cuenta aún necesita una contraseña. Abre el enlace de invitación o pide a un administrador que emita uno nuevo.",
"auth.login.setPasswordFirstTitle": "Establece la contraseña primero:",
"auth.login.setPasswordFirstBody":
"usa el enlace de invitación de tu correo. Si el enlace fue a una dirección antigua (cambio de correo), pide a un administrador de la empresa que emita una nueva invitación para establecer contraseña a {email}.",
"auth.login.yourEmail": "tu correo",
"auth.login.platformAdminsReissue": "Los administradores de plataforma pueden reemitir desde",
"auth.login.adminUsersLink": "Admin → Usuarios",
"auth.login.haveToken": "¿Tienes un token? Abrir aceptar invitación",
"auth.login.noAccount": "¿Sin cuenta?",
"auth.login.createCompany": "Crear empresa",
"auth.login.haveInvite": "¿Tienes una invitación o un enlace para establecer contraseña?",
"auth.login.acceptInvite": "Aceptar invitación",
"auth.register.title": "Crear empresa",
"auth.register.description": "Registra una empresa y su usuario administrador.",
"auth.register.companyName": "Nombre de la empresa",
"auth.register.submit": "Crear cuenta",
"auth.register.submitting": "Creando…",
"auth.register.failed": "Error en el registro",
"auth.register.haveAccount": "¿Ya tienes una cuenta?",
"auth.register.signIn": "Iniciar sesión",
"auth.invite.title": "Aceptar invitación",
"auth.invite.setPasswordTitle": "Establecer contraseña",
"auth.invite.description":
"Establece tu contraseña para unirte a la empresa. Tu administrador asignó Miembro (trabajo diario) o Admin (equipo y facturación).",
"auth.invite.setPasswordDescription":
"Elige una contraseña para tu cuenta Descrybe migrada (al menos 8 caracteres).",
"auth.invite.checking": "Comprobando invitación…",
"auth.invite.forEmail": "Invitación para {email}.",
"auth.invite.linkRecognized":
"Enlace de invitación reconocido. Introduce una contraseña abajo para continuar — el secreto no se muestra en esta página.",
"auth.invite.resetLinkRecognized":
"Enlace de restablecimiento reconocido. Introduce una contraseña abajo para continuar — el secreto no se muestra en esta página.",
"auth.invite.tokenLabel": "Token de invitación",
"auth.invite.resetTokenLabel": "Token de restablecimiento",
"auth.invite.tokenHelp":
"Pega el token del correo de invitación. Se muestra enmascarado en este campo.",
"auth.invite.passwordHint": "Al menos 8 caracteres. Sin otras reglas de complejidad.",
"auth.invite.submit": "Aceptar invitación",
"auth.invite.setPasswordSubmit": "Establecer contraseña",
"auth.invite.accepting": "Aceptando…",
"auth.invite.saving": "Guardando…",
"auth.invite.verifyFailed": "No se pudo verificar la invitación",
"auth.invite.verifySetPasswordFailed":
"No se pudo verificar el enlace para establecer contraseña",
"auth.invite.acceptFailed": "No se pudo aceptar la invitación",
"auth.invite.setPasswordFailed": "No se pudo establecer la contraseña",
"auth.invite.expired":
"Esta invitación no es válida o ha caducado. Pide a tu administrador de la empresa que envíe una nueva invitación y abre el nuevo enlace (o pega el nuevo token abajo).",
"auth.invite.setPasswordExpired":
"Este enlace para establecer contraseña no es válido o ha caducado. Pide a un administrador de la empresa o de la plataforma que lo reemita y abre el nuevo enlace (o pega el nuevo token abajo).",
"auth.invite.emailMismatchDefault":
"Has iniciado sesión con un correo distinto al de esta invitación.",
"auth.invite.expiredFooter":
"¿Enlace caducado? Pide a un administrador que lo reemita — no hay API de reenvío autoservicio. Administradores de plataforma:",
"auth.invite.adminUsersLink": "Admin → Usuarios",
"auth.invite.doneTitle": "Ya formas parte del equipo",
"auth.invite.doneSetPasswordTitle": "Contraseña guardada",
"auth.invite.doneDescription":
"Tu cuenta está lista. A continuación, abre el panel para trabajar con feeds y productos, o revisa la configuración de la empresa.",
"auth.invite.doneSetPasswordDescription":
"Inicia sesión con tu correo y la nueva contraseña para abrir tu espacio de trabajo.",
"auth.invite.openDashboard": "Abrir panel",
"auth.invite.companySettings": "Configuración de la empresa",
"auth.invite.goToSignIn": "Ir a iniciar sesión",
"auth.invite.afterSignInNote":
"Tras iniciar sesión llegas a tu espacio de trabajo — se omite el recorrido de configuración inicial.",
"auth.invite.mismatchTitle": "Cuenta incorrecta para esta invitación",
"auth.invite.mismatchDescription":
"Este enlace es para un correo distinto al de la sesión actual. Cierra sesión para continuar como el usuario invitado, o permanece conectado y pide a un administrador que reemita la invitación.",
"auth.invite.mismatchDetail":
"Sesión iniciada como {session}, pero esta invitación es para {invite}.",
"auth.invite.mismatchFallback": "El correo de la sesión no coincide con esta invitación.",
"auth.invite.switchAccountTitle": "Cambiar de cuenta:",
"auth.invite.switchAccountBody":
"cierra sesión y completa este formulario con el correo invitado{emailSuffix}.",
"auth.invite.reissueTitle": "Vía de reemisión:",
"auth.invite.reissueBody":
"si cambió tu correo real de acceso (desfase de correo), pide a un administrador de la empresa que revoque esta invitación y envíe una nueva al correo con el que inicias sesión. Los administradores de plataforma también pueden reemitir enlaces para establecer contraseña desde Admin → Usuarios.",
"settings.accessDenied":
"No tienes permiso para abrir la configuración de la empresa. Pide ayuda a un administrador de la empresa.",
"settings.profileHeading": "Perfil",
"settings.personalInfo": "Información personal",
"settings.personalInfoHelp": "Actualiza tus datos personales",
"settings.firstName": "Nombre",
"settings.firstNamePlaceholder": "Tu nombre",
"settings.lastName": "Apellidos",
"settings.lastNamePlaceholder": "Tus apellidos",
"settings.email": "Correo electrónico",
"settings.profileUpdated": "Perfil actualizado.",
"settings.profileUpdateFailed": "No se pudo actualizar el perfil",
"settings.role.member": "Miembro",
"settings.role.admin": "Admin",
"settings.teamHeading": "Miembros del equipo",
"settings.inviteUser": "Invitar usuario",
"settings.teamAdminOnly":
"Solo los administradores de la empresa pueden invitar, ascender, degradar o eliminar compañeros.",
"settings.shareAcceptLink": "Compartir enlace de aceptación",
"settings.shareAcceptLinkHelp":
"El correo saliente no está configurado. Copia este enlace de un solo uso y envíaselo al invitado. Establecerá una contraseña (al menos 8 caracteres) y se unirá con el rol que elegiste.",
"settings.acceptLinkLabel": "Enlace de aceptación de invitación de un solo uso",
"settings.copyLink": "Copiar enlace",
"settings.linkCopied": "Enlace de aceptación copiado.",
"settings.table.email": "Correo electrónico",
"settings.table.role": "Rol",
"settings.table.status": "Estado",
"settings.table.joined": "Alta / caduca",
"settings.table.actions": "Acciones",
"settings.teamForbidden":
"No tienes permiso para ver la lista del equipo. Pide ayuda a un administrador de la empresa.",
"settings.noTeammates": "Aún no hay compañeros",
"settings.noTeammatesHelp":
"Invita a colegas como Miembro (productos y feeds) o Admin (equipo y configuración de la empresa). Las invitaciones pendientes aparecen aquí hasta que se acepten.",
"settings.noTeammatesMemberHelp":
"Aún no hay compañeros en la lista. Pide a un administrador de la empresa que envíe invitaciones.",
"settings.memberActions": "Acciones del miembro",
"settings.makeAdmin": "Hacer admin",
"settings.makeMember": "Hacer miembro",
"settings.removeMember": "Eliminar",
"settings.revokeInvite": "Revocar invitación",
"settings.needOneAdmin": "Las empresas necesitan al menos un administrador",
"settings.expires": "Caduca el {date}",
"settings.invalidEmail": "Introduce una dirección de correo válida.",
"settings.inviteCreatedNoMail":
"Invitación creada para {email} como {role}. Copia el enlace de aceptación abajo y compártelo — el correo saliente no está configurado.",
"settings.inviteSent":
"Invitación enviada a {email} como {role}. Debe abrir el correo y aceptar antes de que caduque.",
"settings.inviteFailed": "No se pudo enviar la invitación",
"settings.revokeConfirm": "¿Revocar esta invitación?",
"settings.revoked": "Invitación revocada.",
"settings.revokeFailed": "No se pudo revocar la invitación",
"settings.removeConfirm": "¿Eliminar a {email} de esta empresa?",
"settings.memberRemoved": "{email} eliminado.",
"settings.removeFailed": "No se pudo eliminar al usuario",
"settings.roleChangeConfirm": "¿{action} a {email} a {role}?",
"settings.roleChanged": "{email} ahora es {role}.",
"settings.roleChangeFailed": "No se pudo actualizar el rol",
"settings.promote": "Ascender",
"settings.demote": "Degradar",
"settings.inviteTitle": "Invitar compañero",
"settings.inviteDescription":
"Recibirá un enlace para establecer una contraseña (al menos 8 caracteres) y unirse a esta empresa.",
"settings.inviteEmail": "Correo electrónico",
"settings.inviteEmailPlaceholder": "colega@ejemplo.com",
"settings.inviteRole": "Rol",
"settings.inviteRoleHint":
"Los miembros gestionan productos y feeds. Los administradores también pueden invitar compañeros y cambiar la configuración de la empresa.",
"settings.sendInvite": "Enviar invitación",
"dashboard.demoEmptyHint":
"La zona de pruebas demo está vacía — cambia a A1 o conecta un feed para ver estadísticas reales del catálogo.",
"dashboard.workflowHint": "Importar → enriquecer → publicar. Salta al siguiente paso para {name}.",
"dashboard.overviewHint": "Totales en vivo para {name}",
"dashboard.demoEmptyMessage":
"Cambia a A1 (u otra empresa con datos) en el encabezado, o conecta un feed aquí para poblar esta zona de pruebas.",
"dashboard.emptyTitle": "Aún no hay datos de catálogo",
"dashboard.emptyMessage": "Conecta un feed o sube un CSV para empezar a crear tu catálogo.",
"dashboard.connectFeedAnyway": "Conectar feed de todos modos",
"dashboard.connectFeedShort": "Conectar feed",
"dashboard.uploadCsv": "Subir CSV",
"dashboard.goToBilling": "Ir a Facturación",
"dashboard.freePlanTitle": "Estás en el plan Free",
"dashboard.freePlanMessageWithLimit":
"{used} de {max} productos usados. El mapeo de feeds, la limpieza básica y las etiquetas energéticas de la UE (EPREL) están incluidos; actualiza para títulos y descripciones con IA, y más capacidad.",
"dashboard.freePlanMessage":
"El mapeo de feeds, la limpieza básica y las etiquetas energéticas de la UE (EPREL) están incluidos; actualiza para títulos y descripciones con IA, y más capacidad.",
"dashboard.outOfCreditsTitle": "Te has quedado sin créditos de IA",
"dashboard.outOfCreditsMessage":
"Compra más créditos o actualiza tu plan para seguir procesando.",
"dashboard.productLimitTitle": "Límite de productos alcanzado",
"dashboard.productLimitMessage":
"Tu plan {plan} permite {max} productos ({count} en el catálogo). Actualiza para procesar más.",
"dashboard.productLimitMessageFull":
"Se alcanzó el límite de productos de tu plan {plan}. Actualiza para procesar más.",
"dashboard.comparePlans": "Comparar planes",
"dashboard.viewPlans": "Ver planes",
"dashboard.trialTitle": "Prueba · {plan}",
"dashboard.trialMessageDated": "La prueba termina el {date}. {credits} créditos restantes.",
"dashboard.trialMessage": "{credits} créditos restantes en tu prueba.",
"dashboard.lowCreditsTitle": "Créditos bajos",
"dashboard.lowCreditsMessage":
"Quedan {remaining} de {total} créditos. Recarga o actualiza antes de que se detengan los trabajos.",
"dashboard.latestJobs": "Últimos trabajos de procesamiento",
"dashboard.noJobsEmpty": "Aún no hay trabajos — importa productos primero.",
"dashboard.noJobsReady": "No hay trabajos recientes. Inicia uno desde Productos cuando estés listo.",
"dashboard.startJob": "Iniciar un trabajo",
"dashboard.quickLinksHint": "Feeds, productos, trabajos y exportación.",
"dashboard.feedsImportMap": "Importar y mapear",
"dashboard.productsBrowse": "Explorar y procesar",
"dashboard.jobsMonitor": "Supervisar tareas",
"dashboard.exportsTemplates": "Plantillas y descarga",
"activation.step.enable-fields.title": "Activar campos",
"activation.step.enable-fields.body":
"Activa las columnas de producto estándar que Descrybe mapea y procesa.",
"activation.step.connect-source.title": "Añadir o conectar un origen",
"activation.step.connect-source.body":
"Añade un feed CSV/XML o conecta una tienda para que entren productos.",
"activation.step.map.title": "Mapear campos de origen",
"activation.step.map.body":
"Asocia las columnas del proveedor a los campos de Descrybe y guarda el mapeo.",
"activation.step.sync-sample.title": "Sincronizar una muestra",
"activation.step.sync-sample.body":
"Extrae una muestra pequeña para verificar el mapeo antes de una ejecución completa.",
"activation.step.process.title": "Procesar productos",
"activation.step.process.body":
"Ejecuta el procesamiento sobre productos sincronizados para generar contenido de catálogo limpio.",
"activation.step.export.title": "Exportar",
"activation.step.export.body":
"Crea un feed de exportación para publicar productos limpios como XML o CSV.",
"stats.feeds": "Feeds",
"processing.step.eprel": "EPREL",
"toast.support.replyRe": "Re: {subject}"
}
};
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+278
View File
@@ -0,0 +1,278 @@
/**
* Extra locale strings (auth / settings / dashboard) merged by gen-locale-packs.mjs.
* Brand/loanwords that stay identical across locales are listed in SAME_AS_EN.
*/
export const SAME_AS_EN = new Set([
"admin.settings.integrationsCardDesc",
"admin.settings.providerOption.ollama",
"admin.settings.providerOption.azure",
"admin.settings.providerOption.openrouter",
"admin.settings.providerOption.openai",
"admin.analytics.tokensCol",
"admin.billing.col.total",
"admin.billing.field.term",
"admin.billing.plan",
"admin.chrome.opsBadge",
"admin.chrome.staff.admin",
"admin.diagnostics.colId",
"admin.diagnostics.colStatus",
"admin.knowledge.col.status",
"admin.overview.badge.live",
"admin.overview.tokens",
"admin.plans.col.plan",
"admin.plans.col.term",
"admin.plans.filter.legacy",
"admin.plans.visibility.legacy",
"admin.profile.business.label",
"admin.profile.growth.label",
"admin.profile.legacy.label",
"admin.profile.starter.label",
"admin.settings.allowlist",
"admin.settings.colEmail",
"admin.settings.eprel",
"admin.settings.googleOauth",
"admin.settings.host",
"admin.settings.linkAi",
"admin.settings.linkEmail",
"admin.settings.model",
"admin.settings.namespace",
"admin.settings.pinecone",
"admin.settings.port",
"admin.settings.stripe",
"admin.settings.tab.mail",
"admin.translations.badgeBase",
"admin.translations.colStatus",
"admin.users.colPlan",
"admin.users.colStatus",
"app.name",
"attributes.color",
"attributes.field.keyPlaceholder",
"billing.chartTokens",
"billing.na",
"billing.plan.enterprise",
"billing.skusOf",
"blast.placeholder",
"exports.howTo.google",
"exports.howTo.openapi",
"exports.howTo.rest",
"exports.preset.google_shopping_csv.label",
"exports.preset.google_shopping_xml.defaultName",
"exports.preset.google_shopping_xml.label",
"exports.preset.google_shopping_xml.shortLabel",
"exports.preset.meta_csv.shortLabel",
"feeds.field.urlPlaceholder",
"feeds.sync",
"fields.type.color",
"fields.type.dimension",
"fields.type.image",
"fields.type.url",
"standardFields.type.dimension.label",
"standardFields.type.image.label",
"standardFields.fallback.service",
"standardFields.fallback.eprel_id",
"nav.admin",
"nav.ai",
"nav.feeds",
"nav.section.feeds",
"nav.section.marketing",
"nav.seo",
"processing.actions.export",
"processing.col.status",
"processing.httpError",
"processing.skip",
"processing.step.eprel",
"processing.type.eprel",
"processing.type.seo",
"products.actions.export",
"products.edit.specs",
"products.edit.specsBadge",
"products.edit.status",
"products.edit.tab.feed",
"products.issue.gtin",
"products.table.colFeed",
"products.table.colStatus",
"settings.email",
"settings.inviteEmail",
"settings.inviteEmailPlaceholder",
"settings.keyNamePlaceholder",
"settings.plan",
"settings.role.admin",
"settings.table.email",
"settings.table.status",
"shopify.credentialsDesc.code",
"shopify.setup.step3.after",
"shopify.setup.step4.after",
"shopify.setup.step4.domain",
"standardFields.fallback.color",
"standardFields.fallback.gtin",
"standardFields.fallback.material",
"standardFields.fallback.mpn",
"standardFields.fallback.sku",
"standardFields.fallback.stock",
"standardFields.field.keyPlaceholder",
"standardFields.field.unitPlaceholder",
"standardFields.type.color.label",
"standardFields.type.url.label",
"stats.feeds",
"status.emDash",
"stores.docsPrefix",
"stores.shopify.title",
"stores.woo.title",
"toast.support.replyRe",
"woo.field.consumerKey",
"woo.field.consumerSecret",
"woo.match.ean",
"woo.match.sku",
"woo.setup.step3.after",
"woo.setup.step3.arrow"
]);
/** @type {Record<string, Record<string, string>>} */
export const EXTRA = {};
function fill(locales, map) {
for (const [key, byLocale] of Object.entries(map)) {
for (const [code, text] of Object.entries(byLocale)) {
EXTRA[code] ??= {};
EXTRA[code][key] = text;
}
}
}
fill(
null,
{
"common.askAdmin": {
es: "Pregunta a un administrador de la empresa",
fr: "Demandez à un administrateur de l'entreprise",
de: "Fragen Sie einen Unternehmens-Admin",
it: "Chiedi a un amministratore dell'azienda",
pt: "Peça a um administrador da empresa",
nl: "Vraag een bedrijfsbeheerder",
pl: "Zapytaj administratora firmy",
ja: "会社の管理者に問い合わせる"
},
"common.you": {
es: "Tú",
fr: "Vous",
de: "Sie",
it: "Tu",
pt: "Você",
nl: "U",
pl: "Ty",
ja: "あなた"
},
"common.active": {
es: "Activo",
fr: "Actif",
de: "Aktiv",
it: "Attivo",
pt: "Ativo",
nl: "Actief",
pl: "Aktywny",
ja: "有効"
},
"common.pending": {
es: "Pendiente",
fr: "En attente",
de: "Ausstehend",
it: "In sospeso",
pt: "Pendente",
nl: "In behandeling",
pl: "Oczekujące",
ja: "保留中"
},
"common.email": {
es: "Correo electrónico",
fr: "E-mail",
de: "E-Mail",
it: "Email",
pt: "E-mail",
nl: "E-mail",
pl: "E-mail",
ja: "メール"
},
"common.password": {
es: "Contraseña",
fr: "Mot de passe",
de: "Passwort",
it: "Password",
pt: "Palavra-passe",
nl: "Wachtwoord",
pl: "Hasło",
ja: "パスワード"
},
"common.name": {
es: "Tu nombre",
fr: "Votre nom",
de: "Ihr Name",
it: "Il tuo nome",
pt: "O seu nome",
nl: "Uw naam",
pl: "Twoje imię i nazwisko",
ja: "お名前"
},
"common.viewPricing": {
es: "Ver precios",
fr: "Voir les tarifs",
de: "Preise ansehen",
it: "Vedi i prezzi",
pt: "Ver preços",
nl: "Prijzen bekijken",
pl: "Zobacz cennik",
ja: "料金を見る"
},
"common.backToSignIn": {
es: "Volver a iniciar sesión",
fr: "Retour à la connexion",
de: "Zurück zur Anmeldung",
it: "Torna all'accesso",
pt: "Voltar ao início de sessão",
nl: "Terug naar inloggen",
pl: "Powrót do logowania",
ja: "ログインに戻る"
},
"common.signingOut": {
es: "Cerrando sesión…",
fr: "Déconnexion…",
de: "Abmelden…",
it: "Disconnessione…",
pt: "A terminar sessão…",
nl: "Bezig met uitloggen…",
pl: "Wylogowywanie…",
ja: "ログアウト中…"
},
"common.signOutAndContinue": {
es: "Cerrar sesión y continuar",
fr: "Se déconnecter et continuer",
de: "Abmelden und fortfahren",
it: "Esci e continua",
pt: "Terminar sessão e continuar",
nl: "Uitloggen en doorgaan",
pl: "Wyloguj się i kontynuuj",
ja: "ログアウトして続行"
},
"common.staySignedIn": {
es: "Seguir conectado",
fr: "Rester connecté",
de: "Angemeldet bleiben",
it: "Resta connesso",
pt: "Manter sessão iniciada",
nl: "Ingelogd blijven",
pl: "Pozostań zalogowany",
ja: "ログインしたままにする"
},
"common.startOver": {
es: "Empezar de nuevo",
fr: "Recommencer",
de: "Von vorn beginnen",
it: "Ricomincia",
pt: "Começar de novo",
nl: "Opnieuw beginnen",
pl: "Zacznij od nowa",
ja: "最初から"
}
}
);
// Keep file size manageable: remaining extras live in locale-extra-rest.mjs
+135
View File
@@ -0,0 +1,135 @@
/**
* MERGE-preserve recovery: fill locale values that still equal English
* from phrase-map.json. Never overwrite a value that already differs from en.
*
* Run: node apps/web/scripts/merge-preserve-packs.mjs
*/
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.resolve(__dirname, "../src/lib/i18n/messages");
const mapPath = path.join(__dirname, "phrase-map.json");
const LOCALES = ["es", "fr", "de", "it", "pt", "nl", "pl", "ja"];
const comments = {
es: "Spanish (es) UI pack — keys must stay in sync with en.ts.",
fr: "French (fr) UI pack — keys must stay in sync with en.ts.",
de: "German (de) UI pack — keys must stay in sync with en.ts.",
it: "Italian (it) UI pack — keys must stay in sync with en.ts.",
pt: "Portuguese (pt) UI pack — keys must stay in sync with en.ts.",
nl: "Dutch (nl) UI pack — keys must stay in sync with en.ts.",
pl: "Polish (pl) UI pack — keys must stay in sync with en.ts.",
ja: "Japanese (ja) UI pack — keys must stay in sync with en.ts.",
};
function parseMessageDict(source) {
const dict = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(source))) {
const key = m[1];
const raw = m[2];
dict[key] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return dict;
}
function emitPack(exportName, comment, dict, keyOrder) {
const lines = [
`import type { MessageDict } from "./types";`,
``,
`/** ${comment} */`,
`export const ${exportName}: MessageDict = {`,
];
for (const key of keyOrder) {
lines.push(`\t${JSON.stringify(key)}: ${JSON.stringify(dict[key])},`);
}
lines.push(`};`, ``);
return lines.join("\n");
}
function load(code) {
return parseMessageDict(fs.readFileSync(path.join(messagesDir, `${code}.ts`), "utf8"));
}
if (!fs.existsSync(mapPath)) {
console.error(`missing phrase-map: ${mapPath}`);
process.exit(1);
}
const phraseMap = JSON.parse(fs.readFileSync(mapPath, "utf8"));
const en = load("en");
const keyOrder = Object.keys(en);
let totalApplied = 0;
let totalSkippedGood = 0;
let totalNoMap = 0;
const byLocale = {};
for (const code of LOCALES) {
const pack = load(code);
const next = { ...pack };
let applied = 0;
let skippedGood = 0;
let noMap = 0;
let missingKeys = 0;
for (const key of keyOrder) {
const enVal = en[key];
const cur = next[key];
if (typeof cur === "string" && cur !== enVal) {
skippedGood += 1;
continue;
}
// Identical to EN (or missing) — try phrase-map by English string
const mapped = phraseMap[enVal];
const tr = mapped && typeof mapped[code] === "string" ? mapped[code].trim() : "";
if (tr && tr !== enVal) {
next[key] = mapped[code];
applied += 1;
continue;
}
if (!(key in next)) {
next[key] = enVal;
missingKeys += 1;
}
noMap += 1;
}
fs.writeFileSync(
path.join(messagesDir, `${code}.ts`),
emitPack(code, comments[code], next, keyOrder),
"utf8",
);
byLocale[code] = { applied, skippedGood, noMap, missingKeys };
totalApplied += applied;
totalSkippedGood += skippedGood;
totalNoMap += noMap;
console.log(
`${code}: applied=${applied} skippedGood=${skippedGood} noMap=${noMap} missingKeys=${missingKeys}`,
);
}
console.log("---");
console.log(
JSON.stringify(
{
phraseMapEntries: Object.keys(phraseMap).length,
enKeys: keyOrder.length,
totalApplied,
totalSkippedGood,
totalNoMap,
byLocale,
},
null,
2,
),
);
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
import fs from "node:fs";
import { EXTRA } from "./locale-extra-es.mjs";
function parse(s) {
const d = {};
const re = /"([^"\\]+)"\s*:\s*((?:"(?:\\.|[^"\\])*")|(?:`(?:\\.|[^`\\])*`))/gs;
let m;
while ((m = re.exec(s))) {
const raw = m[2];
d[m[1]] = raw.startsWith("`")
? raw.slice(1, -1).replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\n/g, "\n")
: JSON.parse(raw);
}
return d;
}
const en = parse(fs.readFileSync("../src/lib/i18n/messages/en.ts", "utf8"));
const map = {};
for (const [k, es] of Object.entries(EXTRA.es)) {
const e = en[k];
if (e) map[e] = { ...(map[e] || {}), es };
}
fs.writeFileSync("_phrase-seed.json", JSON.stringify(map, null, 2));
console.log(Object.keys(map).length);
+80
View File
@@ -0,0 +1,80 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const messagesDir = path.join(__dirname, "..", "src", "lib", "i18n", "messages");
const locales = ["de", "es", "fr", "it", "ja", "nl", "pl", "pt"];
const names = {
de: "German (de)",
es: "Spanish (es)",
fr: "French (fr)",
it: "Italian (it)",
ja: "Japanese (ja)",
nl: "Dutch (nl)",
pl: "Polish (pl)",
pt: "Portuguese (pt)"
};
/** Parse flat `"key": "value",` MessageDict bodies (JSON-string keys/values). */
function parseMessageDict(source) {
const start = source.indexOf("{");
const end = source.lastIndexOf("}");
if (start < 0 || end <= start) {
throw new Error("MessageDict object braces not found");
}
const body = source.slice(start, end + 1);
// Keys/values are JSON strings in this repo's packs.
const out = {};
const re = /("(?:\\.|[^"\\])*")\s*:\s*("(?:\\.|[^"\\])*")\s*,?/g;
let m;
while ((m = re.exec(body)) !== null) {
out[JSON.parse(m[1])] = JSON.parse(m[2]);
}
return out;
}
function writePack(code, dict, enKeys) {
const lines = [
'import type { MessageDict } from "./types";',
"",
`/** ${names[code]} UI pack — keys must stay in sync with en.ts. */`,
`export const ${code}: MessageDict = {`
];
for (const k of enKeys) {
lines.push(`\t${JSON.stringify(k)}: ${JSON.stringify(dict[k])},`);
}
lines.push("};", "");
fs.writeFileSync(path.join(messagesDir, `${code}.ts`), lines.join("\n"), "utf8");
}
const enSource = fs.readFileSync(path.join(messagesDir, "en.ts"), "utf8");
const en = parseMessageDict(enSource);
const enKeys = Object.keys(en);
let totalFilled = 0;
let totalRemoved = 0;
for (const code of locales) {
const filePath = path.join(messagesDir, `${code}.ts`);
const pack = parseMessageDict(fs.readFileSync(filePath, "utf8"));
const extras = Object.keys(pack).filter((k) => !(k in en));
const next = {};
let filled = 0;
for (const k of enKeys) {
const cur = pack[k];
if (cur == null || !String(cur).trim()) {
next[k] = en[k];
filled += 1;
} else {
next[k] = cur;
}
}
totalFilled += filled;
totalRemoved += extras.length;
writePack(code, next, enKeys);
console.log(`${code}: filled=${filled} removed_extra=${extras.length} keys=${enKeys.length}`);
}
console.log(`DONE filled_total=${totalFilled} removed_total=${totalRemoved}`);