Greasy Fork is available in English.
Popmundo script ekosistemi için merkezi çeviri ve dil modülü
// ==UserScript==
// @name 🌍 PopLang
// @name:tr 🌍 PopLang
// @name:en 🌍 PopLang
// @name:pt-BR 🌍 PopLang
// @namespace popmundo.poplang
// @version 1.0
// @description Popmundo script ekosistemi için merkezi çeviri ve dil modülü
// @description:en Centralized language & translation module for the Popmundo script ecosystem
// @description:pt-BR Módulo central de idioma e tradução para o ecossistema de scripts Popmundo
// @author luke-james-gibson
// @license MIT
// @match https://*.popmundo.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
// ─── COOKIE HELPER ─────────────────────────────────────────────────────────────
const CK = {
get: k => { const m = document.cookie.match(new RegExp('(?:^|; )' + k + '=([^;]*)')); return m ? decodeURIComponent(m[1]) : null; },
set: (k, v) => { document.cookie = `${k}=${encodeURIComponent(v)};domain=.popmundo.com;path=/;max-age=31536000`; },
};
// ─── DOM HELPER ────────────────────────────────────────────────────────────────
const mk = (tag, txt) => { const e = document.createElement(tag); if (txt != null) e.textContent = txt; return e; };
const mkB = (txt, fn) => Object.assign(mk('button', txt), { onclick: fn, type: 'button' });
// ─── LANGUAGE STATE ────────────────────────────────────────────────────────────
// 'PT' cookie değerini PT-BR'ye normalize et (eski sürüm uyumluluğu)
const _normLang = l => (l === 'PT' ? 'PT-BR' : l) || 'TR';
let _lang = _normLang(CK.get('ppm_lang'));
// ─── LOCALSTORAGE KEYS ─────────────────────────────────────────────────────────
const _SCRIPT_IDS = ['popcontrol', 'helper', 'social', 'social_mobile', 'depot', 'guide', 'route49'];
const _lsKey = id => 'ppl_lc_' + id; // yeni prefix
const _lsKeyOld = id => 'ppc_lc_' + id; // eski prefix (migration için)
// ─── MIGRATION: ppc_lc_* → ppl_lc_* ───────────────────────────────────────────
_SCRIPT_IDS.forEach(id => {
const nk = _lsKey(id), ok = _lsKeyOld(id);
if (!localStorage.getItem(nk) && localStorage.getItem(ok)) {
localStorage.setItem(nk, localStorage.getItem(ok));
localStorage.removeItem(ok);
}
});
// ─── CUSTOM OVERRIDES ──────────────────────────────────────────────────────────
// { scriptId: { key: value } } — localStorage'dan yüklenir
const _custom = {};
_SCRIPT_IDS.forEach(id => {
try { const v = localStorage.getItem(_lsKey(id)); if (v) _custom[id] = JSON.parse(v); } catch {}
});
// ═══════════════════════════════════════════════════════════════════════════════
// STRING REGISTRY
// Format: { scriptId: { TR: {}, EN: {}, 'PT-BR': {} } }
// ═══════════════════════════════════════════════════════════════════════════════
const _S = {
// ─── POPLANG (kendi UI metinleri) ──────────────────────────────────────────────
poplang: {
TR: {
menuLabel: '🌍 Dil Ayarları',
title: '🌍 PopLang — Dil Ayarları',
langLabel: 'Dil Seçin',
customizeBtn: '🌍 Özel Dil',
custTitle: '🌍 Özel Dil (Customize)',
custInfo: 'Tüm scriptlerin arayüzünü istediğiniz dile çevirebilirsiniz:',
custStep1: '📋 "Export JSON\'u Kopyala" düğmesine bas',
custStep2: 'Google Gemini\'ye yapıştır, hedef dili belirt',
custStep3: 'Dönen JSON\'u "Import" ile yükle',
custStep4: 'Tüm scriptler anında yeni dile geçer',
exportLabel: '📤 Export',
exportBtn: '📋 Export JSON\'u Kopyala',
exportEmpty: '⚠️ Henüz kayıtlı string yok',
exportCopied: '✅ Kopyalandı!',
importLabel: '📥 Import',
importPH: '{ "depot": { "btnClose": "Schließen", ... }, ... }',
importApply: '✅ Uygula & Yenile',
importPasteFirst: 'Önce çevrilmiş JSON yapıştırın.',
importNoMatch: 'Eşleşen script anahtarı bulunamadı.',
importApplied: '✅ {n} script\'e uygulandı — yenileniyor...',
importError: '❌ Geçersiz JSON: ',
resetBtn: '🗑️ Varsayılan Dile Dön',
resetConfirm: 'Tüm özel dil verisi kaldırılsın mı?',
close: 'Kapat',
hasCustomBadge: 'ÖZEL DİL AKTİF',
},
EN: {
menuLabel: '🌍 Language Settings',
title: '🌍 PopLang — Language Settings',
langLabel: 'Select Language',
customizeBtn: '🌍 Custom Language',
custTitle: '🌍 Custom Language (Customize)',
custInfo: 'You can translate all script interfaces to any language:',
custStep1: '📋 Click "Copy Export JSON"',
custStep2: 'Paste into Google Gemini, specify the target language',
custStep3: 'Load the returned JSON via Import',
custStep4: 'All scripts switch to the new language instantly',
exportLabel: '📤 Export',
exportBtn: '📋 Copy Export JSON',
exportEmpty: '⚠️ No strings registered yet',
exportCopied: '✅ Copied!',
importLabel: '📥 Import',
importPH: '{ "depot": { "btnClose": "Schließen", ... }, ... }',
importApply: '✅ Apply & Reload',
importPasteFirst: 'Paste translated JSON first.',
importNoMatch: 'No matching script keys found.',
importApplied: '✅ Applied to {n} script(s) — reloading...',
importError: '❌ Invalid JSON: ',
resetBtn: '🗑️ Reset to Default Language',
resetConfirm: 'Remove all custom language data and restore defaults?',
close: 'Close',
hasCustomBadge: 'CUSTOM LANG ACTIVE',
},
'PT-BR': {
menuLabel: '🌍 Configurações de Idioma',
title: '🌍 PopLang — Configurações de Idioma',
langLabel: 'Selecionar Idioma',
customizeBtn: '🌍 Idioma Personalizado',
custTitle: '🌍 Idioma Personalizado (Customize)',
custInfo: 'Você pode traduzir todos os scripts para qualquer idioma:',
custStep1: '📋 Clique em "Copiar JSON de Exportação"',
custStep2: 'Cole no Google Gemini, especifique o idioma alvo',
custStep3: 'Carregue o JSON retornado via Importar',
custStep4: 'Todos os scripts mudam para o novo idioma instantaneamente',
exportLabel: '📤 Exportar',
exportBtn: '📋 Copiar JSON de Exportação',
exportEmpty: '⚠️ Nenhuma string registrada ainda',
exportCopied: '✅ Copiado!',
importLabel: '📥 Importar',
importPH: '{ "depot": { "btnClose": "Schließen", ... }, ... }',
importApply: '✅ Aplicar & Recarregar',
importPasteFirst: 'Cole o JSON traduzido primeiro.',
importNoMatch: 'Nenhuma chave de script correspondente encontrada.',
importApplied: '✅ Aplicado a {n} script(s) — recarregando...',
importError: '❌ JSON inválido: ',
resetBtn: '🗑️ Restaurar Idioma Padrão',
resetConfirm: 'Remover todos os dados de idioma personalizado?',
close: 'Fechar',
hasCustomBadge: 'IDIOMA PERSONALIZADO ATIVO',
},
},
// ─── POPCONTROL ────────────────────────────────────────────────────────────────
popcontrol: {
TR: {
posLabel: 'Konum',
posBottom: '⬇ Alt',
posTop: '⬆ Üst',
posLeft: '◀ Sol',
posRight: '▶ Sağ',
langLabel: 'Dil',
langOpen: '🌍 Dil Ayarları',
scripts: 'Scriptler',
order: 'Sıra',
orderHint: 'Barda butonları sürükleyip bırakarak sıralayabilirsiniz.',
colorSettings: 'Renk Ayarları',
colorBar: 'Bar Rengi',
colorText: 'Yazı Rengi',
shortcut: 'Paneli Aç/Kapat - Alt + P',
charMenu: 'Karakter Menüsü',
charMenuDesc: 'Gizle, favorilere al, sırala',
manage: 'Yönet',
collapse: 'Küçült',
close: 'Kapat',
settings: 'PopControl',
resetAll: '🔄 Tümünü Sıfırla',
resetConfirm: 'Tüm gizleme ve favori ayarları sıfırlansın mı?',
readTR: 'Beni Oku',
readEN: 'Read Me',
readPT: 'Leia-me',
},
EN: {
posLabel: 'Position',
posBottom: '⬇ Bottom',
posTop: '⬆ Top',
posLeft: '◀ Left',
posRight: '▶ Right',
langLabel: 'Language',
langOpen: '🌍 Language Settings',
scripts: 'Scripts',
order: 'Order',
orderHint: 'Drag & drop buttons on the bar to reorder.',
colorSettings: 'Color Settings',
colorBar: 'Bar Color',
colorText: 'Text Color',
shortcut: 'Toggle Panel - Alt + P',
charMenu: 'Character Menu',
charMenuDesc: 'Hide, favorite & reorder items',
manage: 'Manage',
collapse: 'Collapse',
close: 'Close',
settings: 'PopControl',
resetAll: '🔄 Reset All',
resetConfirm: 'Reset all hide/favorite settings?',
readTR: 'Beni Oku',
readEN: 'Read Me',
readPT: 'Leia-me',
},
'PT-BR': {
posLabel: 'Posição',
posBottom: '⬇ Baixo',
posTop: '⬆ Cima',
posLeft: '◀ Esquerda',
posRight: '▶ Direita',
langLabel: 'Idioma',
langOpen: '🌍 Configurações de Idioma',
scripts: 'Scripts',
order: 'Ordem',
orderHint: 'Arraste os botões na barra para reordenar.',
colorSettings: 'Configurações de Cor',
colorBar: 'Cor da Barra',
colorText: 'Cor do Texto',
shortcut: 'Alternar Painel - Alt + P',
charMenu: 'Menu do Personagem',
charMenuDesc: 'Ocultar, favoritar e reordenar',
manage: 'Gerenciar',
collapse: 'Recolher',
close: 'Fechar',
settings: 'PopControl',
resetAll: '🔄 Redefinir Tudo',
resetConfirm: 'Redefinir todas as configurações?',
readTR: 'Beni Oku',
readEN: 'Read Me',
readPT: 'Leia-me',
},
},
// ─── DEPOT ─────────────────────────────────────────────────────────────────────
depot: {
TR: {
scanTitle: '🔄 Otomatik Tarama',
scanPersonal: '👤 Kişisel Eşyalar',
scanVehicles: '🚗 Kişisel Araçlar',
scanHousing: '🏠 Evler',
scanArtist: '🎵 Sanatçı / Turne Aracı',
scanWarn: '⚠️ Tarama sırasında sekmeler otomatik değişir.',
scanCancel: 'İptal',
scanStart: '▶ Taramayı Başlat',
scanSelectOne: 'En az bir kategori seçin.',
scanDone: '✅ Tarama Tamamlandı',
scanLocCount: 'Taranan Lokasyon:',
scanItemCount: 'Toplam Eşya:',
scanClose: 'Kapat',
scanAskId: 'Karakter ID girin:',
scanBadId: 'Geçerli ID girilmedi.',
scanVehList: '🔍 Araç listesi:',
scanVehUnit: 'araç.',
scanHouseList: '🔍 Ev listesi:',
scanHouseUnit: 'ev.',
scanLogOk: 'çeşit.',
scanLogEmpty: 'eşya yok.',
scanStop: '🛑 DURDUR',
panelTitle: '📦 Depot',
dragHint: '⠿ Sürükle',
btnSettings: 'Panel Ayarları',
btnBackup: '📤 Scripti Yedekle',
btnRestore: '📥 Yedeği Yükle',
catLabel: 'Kategori CSV (Kategori,Eşya):',
catPlaceholder: 'Kategori,Eşya Adı',
btnCatSave: '💾 Kategorileri Güncelle',
btnClearAll: '⚠️ Tüm Veriyi Sıfırla',
btnSavePage: '📍 Sayfayı Kaydet',
btnDelRecord: '🗑️ Kaydı Sil',
btnInventory: 'Envanter İşlemleri',
btnAutoScan: '🔄 OTOMATİK TARA',
btnListManage: '📋 ENVANTERLERİ LİSTELE / YÖNET',
btnDetailed: '📄 Detaylı CSV',
btnStockCSV: '📊 Stok & Fiyatlı CSV',
btnImportCSV: '📥 CSV Yükle',
btnForumList: '💰 Fiyat Kontrolü',
csvEditorTitle: '📊 CSV Görüntüleyici / Düzenleyici',
csvFormat: 'CSV Formatı:',
csvFmtDetailed: 'Detaylı CSV (Tüm Bilgiler)',
csvFmtSummary: 'Özet CSV (Stok & Fiyat)',
csvFmtForum: 'Forum Formatı (Fiyat Listesi)',
csvDataLabel: 'CSV Verisi (düzenlenebilir):',
csvCopy: '📋 Kopyala',
csvDownload: '💾 İndir',
csvApply: '✓ Değişiklikleri Uygula',
csvUpdated: 'CSV verisi güncellendi.',
csvCopied: 'CSV panoya kopyalandı!',
csvNoPrice: 'Güncellenecek fiyat bulunamadı.',
csvApplyUnsup: 'Bu format için değişiklik uygulama desteklenmiyor.',
csvErrPrefix: 'Hata: ',
pcTitle: '💰 Fiyat Kontrolü',
pcPasteLbl: 'Forum listesi veya oyun içi mesajı buraya yapıştırın:',
pcPastePh: 'Snowglobe|1|100m\nveya yapışık:\nPhony Bad Teeth x1Snowball x1',
pcProcess: 'İşle',
pcAIBtn: '🤖 AI ile Temizle',
pcAIHint: "Prompt kopyalandı! Gemini'ye açıldı.",
pcClose: 'Kapat',
pcFound: 'Sende:',
pcNotFound: 'Envanterde yok',
pcForumPrice: 'Girilen fiyat:',
pcLastPrice: 'Son fiyat:',
pcAddPrice: 'Fiyatı Ekle',
pcAdded: 'Eklendi ✓',
pcHistory: 'Geçmiş',
pcNoHistory: 'Fiyat geçmişi yok.',
pcNoInput: 'Lütfen bir liste girin.',
pcResults: 'Sonuçlar:',
pcApplyAll: 'Tümünü Uygula',
pcApplied: 'fiyat uygulandı.',
searchPH: 'Envanterde ara (Örn: Haiku)...',
alertNoItems: 'Bu sayfada eşya bulunamadı.',
alertDeleted: 'Silindi.',
alertNotFound: 'Kayıt bulunamadı.',
alertEnterData: 'Veri girin.',
alertCatFmt: "Format hatalı! 'Kategori,Eşya'",
alertClearConf: 'Tüm DB sıfırlanacak!',
alertRestored: 'Geri yüklendi.',
alertInvalidFile: 'Geçersiz dosya.',
listEmpty: 'Veritabanı boş.',
btnDelAll: 'Tümünü Sil',
btnDel: 'SİL',
confirmDel: 'Bu kayıt silinsin mi?',
noResults: 'Sonuç bulunamadı.',
totalLabel: 'Toplam',
lastScanLabel: 'Son tarama:',
btnCopy: '📋 Bu sonuçları kopyala',
btnCopied: '✅ Kopyalandı',
noPriceLabel: 'Teklif ediniz',
clsPersonal: 'Eşyalar',
clsVehicle: 'Kişisel Araç',
clsHousing: 'Ev',
clsArtist: 'Sanatçı Aracı',
minTooltip: 'Paneli aç',
btnMinimize: 'Küçült',
btnClose: 'Kapat',
locVehicleFb: 'Kişisel Araç',
locLocaleFb: 'Mekan',
locCharFb: 'Karakter',
locInvSuffix: 'Envanteri',
locArtistFb: 'Grup Aracı',
locUnknown: 'Bilinmeyen',
variantDefault: 'Standart',
},
EN: {
scanTitle: '🔄 Auto Scan',
scanPersonal: '👤 Personal Items',
scanVehicles: '🚗 Personal Vehicles',
scanHousing: '🏠 Housing',
scanArtist: '🎵 Artist / Tour Vehicle',
scanWarn: '⚠️ Tabs will change automatically during scan.',
scanCancel: 'Cancel',
scanStart: '▶ Start Scan',
scanSelectOne: 'Select at least one category.',
scanDone: '✅ Scan Complete',
scanLocCount: 'Scanned Locations:',
scanItemCount: 'Total Items:',
scanClose: 'Close',
scanAskId: 'Enter Character ID:',
scanBadId: 'No valid ID entered.',
scanVehList: '🔍 Vehicle list:',
scanVehUnit: 'vehicles.',
scanHouseList: '🔍 Housing list:',
scanHouseUnit: 'properties.',
scanLogOk: 'types.',
scanLogEmpty: 'no items.',
scanStop: '🛑 STOP',
panelTitle: '📦 Depot',
dragHint: '⠿ Drag',
btnSettings: 'Panel Settings',
btnBackup: '📤 Backup Script',
btnRestore: '📥 Load Backup',
catLabel: 'Category CSV (Category,Item):',
catPlaceholder: 'Category,Item Name',
btnCatSave: '💾 Update Categories',
btnClearAll: '⚠️ Reset All Data',
btnSavePage: '📍 Save Page',
btnDelRecord: '🗑️ Delete Record',
btnInventory: 'Inventory Actions',
btnAutoScan: '🔄 AUTO SCAN',
btnListManage: '📋 LIST / MANAGE INVENTORIES',
btnDetailed: '📄 Detailed CSV',
btnStockCSV: '📊 Stock & Priced CSV',
btnImportCSV: '📥 Import CSV',
btnForumList: '💰 Price Check',
csvEditorTitle: '📊 CSV Viewer / Editor',
csvFormat: 'CSV Format:',
csvFmtDetailed: 'Detailed CSV (All Info)',
csvFmtSummary: 'Summary CSV (Stock & Price)',
csvFmtForum: 'Forum Format (Price List)',
csvDataLabel: 'CSV Data (editable):',
csvCopy: '📋 Copy',
csvDownload: '💾 Download',
csvApply: '✓ Apply Changes',
csvUpdated: 'CSV data updated.',
csvCopied: 'CSV copied to clipboard!',
csvNoPrice: 'No prices to update.',
csvApplyUnsup: 'Applying changes not supported for this format.',
csvErrPrefix: 'Error: ',
pcTitle: '💰 Price Check',
pcPasteLbl: 'Paste forum list or in-game message here:',
pcPastePh: 'Snowglobe|1|100m\nor concatenated:\nPhony Bad Teeth x1Snowball x1',
pcProcess: 'Process',
pcAIBtn: '🤖 Clean with AI',
pcAIHint: 'Prompt copied! Gemini opened.',
pcClose: 'Close',
pcFound: 'You have:',
pcNotFound: 'Not in inventory',
pcForumPrice: 'Listed price:',
pcLastPrice: 'Last price:',
pcAddPrice: 'Add Price',
pcAdded: 'Added ✓',
pcHistory: 'History',
pcNoHistory: 'No price history.',
pcNoInput: 'Please enter a list.',
pcResults: 'Results:',
pcApplyAll: 'Apply All',
pcApplied: 'prices applied.',
searchPH: 'Search inventory (e.g. Haiku)...',
alertNoItems: 'No items found on this page.',
alertDeleted: 'Deleted.',
alertNotFound: 'Record not found.',
alertEnterData: 'Enter data.',
alertCatFmt: "Invalid format! 'Category,Item'",
alertClearConf: 'All data will be reset!',
alertRestored: 'Restored.',
alertInvalidFile: 'Invalid file.',
listEmpty: 'Database empty.',
btnDelAll: 'Delete All',
btnDel: 'DEL',
confirmDel: 'Delete this record?',
noResults: 'No results found.',
totalLabel: 'Total',
lastScanLabel: 'Last scan:',
btnCopy: '📋 Copy these results',
btnCopied: '✅ Copied',
noPriceLabel: 'Make an offer',
clsPersonal: 'Items',
clsVehicle: 'Personal Vehicle',
clsHousing: 'Housing',
clsArtist: 'Artist Vehicle',
minTooltip: 'Open panel',
btnMinimize: 'Minimize',
btnClose: 'Close',
locVehicleFb: 'Personal Vehicle',
locLocaleFb: 'Locale',
locCharFb: 'Character',
locInvSuffix: 'Inventory',
locArtistFb: 'Artist Vehicle',
locUnknown: 'Unknown',
variantDefault: 'Standard',
},
'PT-BR': {
scanTitle: '🔄 Varredura Automática',
scanPersonal: '👤 Itens Pessoais',
scanVehicles: '🚗 Veículos Pessoais',
scanHousing: '🏠 Moradias',
scanArtist: '🎵 Artista / Veículo de Turnê',
scanWarn: '⚠️ As abas mudarão automaticamente durante a varredura.',
scanCancel: 'Cancelar',
scanStart: '▶ Iniciar Varredura',
scanSelectOne: 'Selecione pelo menos uma categoria.',
scanDone: '✅ Varredura Concluída',
scanLocCount: 'Locais Varridos:',
scanItemCount: 'Total de Itens:',
scanClose: 'Fechar',
scanAskId: 'Digite o ID do Personagem:',
scanBadId: 'Nenhum ID válido inserido.',
scanVehList: '🔍 Lista de veículos:',
scanVehUnit: 'veículos.',
scanHouseList: '🔍 Lista de moradias:',
scanHouseUnit: 'imóveis.',
scanLogOk: 'tipos.',
scanLogEmpty: 'sem itens.',
scanStop: '🛑 PARAR',
panelTitle: '📦 Depot',
dragHint: '⠿ Arrastar',
btnSettings: 'Configurações do Painel',
btnBackup: '📤 Fazer Backup',
btnRestore: '📥 Carregar Backup',
catLabel: 'CSV de Categorias (Categoria,Item):',
catPlaceholder: 'Categoria,Nome do Item',
btnCatSave: '💾 Atualizar Categorias',
btnClearAll: '⚠️ Redefinir Todos os Dados',
btnSavePage: '📍 Salvar Página',
btnDelRecord: '🗑️ Excluir Registro',
btnInventory: 'Ações de Inventário',
btnAutoScan: '🔄 VARREDURA AUTOMÁTICA',
btnListManage: '📋 LISTAR / GERENCIAR INVENTÁRIOS',
btnDetailed: '📄 CSV Detalhado',
btnStockCSV: '📊 CSV de Estoque & Preços',
btnImportCSV: '📥 Importar CSV',
btnForumList: '💰 Verificação de Preço',
csvEditorTitle: '📊 Visualizador / Editor CSV',
csvFormat: 'Formato CSV:',
csvFmtDetailed: 'CSV Detalhado (Todas as Informações)',
csvFmtSummary: 'CSV Resumido (Estoque & Preço)',
csvFmtForum: 'Formato Fórum (Lista de Preços)',
csvDataLabel: 'Dados CSV (editável):',
csvCopy: '📋 Copiar',
csvDownload: '💾 Baixar',
csvApply: '✓ Aplicar Alterações',
csvUpdated: 'Dados CSV atualizados.',
csvCopied: 'CSV copiado para a área de transferência!',
csvNoPrice: 'Nenhum preço para atualizar.',
csvApplyUnsup: 'Aplicação não suportada para este formato.',
csvErrPrefix: 'Erro: ',
pcTitle: '💰 Verificação de Preço',
pcPasteLbl: 'Cole a lista do fórum ou mensagem do jogo aqui:',
pcPastePh: 'Snowglobe|1|100m\nou concatenado:\nPhony Bad Teeth x1Snowball x1',
pcProcess: 'Processar',
pcAIBtn: '🤖 Limpar com IA',
pcAIHint: 'Prompt copiado! Gemini aberto.',
pcClose: 'Fechar',
pcFound: 'Você tem:',
pcNotFound: 'Não está no inventário',
pcForumPrice: 'Preço listado:',
pcLastPrice: 'Último preço:',
pcAddPrice: 'Adicionar Preço',
pcAdded: 'Adicionado ✓',
pcHistory: 'Histórico',
pcNoHistory: 'Sem histórico de preços.',
pcNoInput: 'Por favor, insira uma lista.',
pcResults: 'Resultados:',
pcApplyAll: 'Aplicar Todos',
pcApplied: 'preços aplicados.',
searchPH: 'Buscar no inventário (ex: Haiku)...',
alertNoItems: 'Nenhum item encontrado nesta página.',
alertDeleted: 'Excluído.',
alertNotFound: 'Registro não encontrado.',
alertEnterData: 'Insira os dados.',
alertCatFmt: "Formato inválido! 'Categoria,Item'",
alertClearConf: 'Todos os dados serão redefinidos!',
alertRestored: 'Restaurado.',
alertInvalidFile: 'Arquivo inválido.',
listEmpty: 'Banco de dados vazio.',
btnDelAll: 'Excluir Tudo',
btnDel: 'EXC',
confirmDel: 'Excluir este registro?',
noResults: 'Nenhum resultado encontrado.',
totalLabel: 'Total',
lastScanLabel: 'Última varredura:',
btnCopy: '📋 Copiar estes resultados',
btnCopied: '✅ Copiado',
noPriceLabel: 'Consultar preço',
clsPersonal: 'Itens',
clsVehicle: 'Veículo Pessoal',
clsHousing: 'Moradia',
clsArtist: 'Veículo do Artista',
minTooltip: 'Abrir painel',
btnMinimize: 'Minimizar',
btnClose: 'Fechar',
locVehicleFb: 'Veículo Pessoal',
locLocaleFb: 'Local',
locCharFb: 'Personagem',
locInvSuffix: 'Inventário',
locArtistFb: 'Veículo do Artista',
locUnknown: 'Desconhecido',
variantDefault: 'Padrão',
},
},
// ─── DİĞER SCRIPTLERr — her refactor sırasında doldurulacak ───────────────────
helper: { TR: {}, EN: {}, 'PT-BR': {} },
social: { TR: {}, EN: {}, 'PT-BR': {} },
social_mobile: { TR: {}, EN: {}, 'PT-BR': {} },
guide: { TR: {}, EN: {}, 'PT-BR': {} },
route49: { TR: {}, EN: {}, 'PT-BR': {} },
}; // _S sonu
// ─── KISA ERİŞİM: PopLang kendi UI metinleri ───────────────────────────────────
const _ui = k => _S.poplang[_lang]?.[k] ?? _S.poplang.TR?.[k] ?? k;
// ═══════════════════════════════════════════════════════════════════════════════
// PUBLIC API
// ═══════════════════════════════════════════════════════════════════════════════
const API = {
/**
* Bir script için çevrilmiş string döner.
* Öncelik: custom override > aktif dil > TR fallback > key kendisi
* @param {string} scriptId — 'depot', 'helper', vb.
* @param {string} key
* @returns {string}
*/
get(scriptId, key) {
if (_custom[scriptId]?.[key]) return _custom[scriptId][key];
const dict = _S[scriptId];
if (!dict) return key;
return dict[_lang]?.[key] ?? dict.TR?.[key] ?? key;
},
/**
* Bir script için tüm çeviri objesini döner.
* Scriptler başlangıçta bunu çekip lokal cache'e alabilir:
* const S = window.PopLang?.getAll('depot') ?? {};
* const _t = k => S[k] ?? k;
* @param {string} scriptId
* @returns {object}
*/
getAll(scriptId) {
const dict = _S[scriptId];
if (!dict) return {};
// TR taban, üstüne aktif dil, üstüne custom
const merged = Object.assign({}, dict.TR || {}, dict[_lang] || {}, _custom[scriptId] || {});
return merged;
},
/** Aktif dil kodu: 'TR' | 'EN' | 'PT-BR' */
getLang: () => _lang,
/** Dili ayarla ve cookie'ye yaz (reload scriptlere kalır) */
setLang(lang) {
_lang = _normLang(lang);
CK.set('ppm_lang', _lang);
},
/** Dil seçici + Customize panelini aç */
openUI: () => _openUI(),
/**
* Script kendi TR stringlerini runtime'da ekleyebilir (opsiyonel, forward-compat)
* Gerekirse custom stringlerini register etmek için kullanılır.
*/
register(scriptId, tr, en, ptBr) {
if (!_S[scriptId]) _S[scriptId] = { TR: {}, EN: {}, 'PT-BR': {} };
if (tr) Object.assign(_S[scriptId].TR, tr);
if (en) Object.assign(_S[scriptId].EN, en);
if (ptBr) Object.assign(_S[scriptId]['PT-BR'], ptBr);
},
/** Tüm string registry'sini döner (Customize export için) */
_getRegistry: () => _S,
/** Versiyon */
version: '1.0',
};
// Global erişim
window.PopLang = API;
if (typeof unsafeWindow !== 'undefined') unsafeWindow.PopLang = API;
// ═══════════════════════════════════════════════════════════════════════════════
// UI — DİL SEÇİCİ + CUSTOMIZE PANELİ
// ═══════════════════════════════════════════════════════════════════════════════
function _openUI() {
document.getElementById('ppl-ov')?.remove();
const hasCustom = Object.keys(_custom).length > 0;
const ov = document.createElement('div');
ov.id = 'ppl-ov';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:100000;display:flex;align-items:flex-end;justify-content:center';
const box = document.createElement('div');
box.style.cssText = 'background:#fff;border-radius:16px 16px 0 0;padding:20px;width:100%;max-width:520px;max-height:85vh;overflow-y:auto;box-sizing:border-box;font-family:inherit';
const mkH = t => {
const d = document.createElement('div');
d.textContent = t;
d.style.cssText = 'font-size:10px;font-weight:700;color:#888;text-transform:uppercase;letter-spacing:.5px;margin:16px 0 6px';
return d;
};
const mkHr = () => { const hr = document.createElement('hr'); hr.style.cssText = 'border:none;border-top:1px solid #eee;margin:12px 0'; return hr; };
// ── Başlık ───────────────────────────────────────────────────────────────
const hdr = document.createElement('div');
hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:16px';
const title = document.createElement('div');
title.style.cssText = 'display:flex;align-items:center;gap:8px';
const titleText = document.createElement('span');
titleText.textContent = _ui('title');
titleText.style.cssText = 'font-weight:700;font-size:15px';
title.appendChild(titleText);
if (hasCustom) {
const badge = document.createElement('span');
badge.textContent = _ui('hasCustomBadge');
badge.style.cssText = 'font-size:9px;font-weight:700;background:#f39c12;color:#fff;padding:2px 7px;border-radius:10px;letter-spacing:.3px';
title.appendChild(badge);
}
const xBtn = mkB('✕', () => ov.remove());
xBtn.style.cssText = 'background:none;border:none;font-size:20px;cursor:pointer;color:#aaa;padding:0';
hdr.appendChild(title);
hdr.appendChild(xBtn);
box.appendChild(hdr);
// ── Dil Seçimi ───────────────────────────────────────────────────────────
box.appendChild(mkH(_ui('langLabel')));
const langRow = document.createElement('div');
langRow.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap';
const langs = [
{ code: 'TR', label: '🇹🇷 Türkçe' },
{ code: 'EN', label: '🇬🇧 English' },
{ code: 'PT-BR', label: '🇧🇷 Português' },
];
langs.forEach(({ code, label }) => {
const b = mkB(label, () => {
API.setLang(code);
ov.remove();
location.reload();
});
const isActive = _lang === code;
b.style.cssText = `flex:1;min-width:100px;padding:9px 6px;border-radius:8px;border:2px solid ${isActive ? '#6f42c1' : '#ddd'};background:${isActive ? '#6f42c1' : '#f8f9fa'};color:${isActive ? '#fff' : '#333'};font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s`;
langRow.appendChild(b);
});
// Customize butonu
const custBtn = mkB(_ui('customizeBtn'), () => { ov.remove(); _openCustomize(); });
const isCust = hasCustom;
custBtn.style.cssText = `flex:1;min-width:100px;padding:9px 6px;border-radius:8px;border:2px solid ${isCust ? '#f39c12' : '#ddd'};background:${isCust ? '#fff8ee' : '#f8f9fa'};color:${isCust ? '#f39c12' : '#555'};font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .15s`;
langRow.appendChild(custBtn);
box.appendChild(langRow);
box.appendChild(mkHr());
// ── Versiyon bilgisi ─────────────────────────────────────────────────────
const vRow = document.createElement('div');
vRow.style.cssText = 'text-align:center;font-size:11px;color:#aaa;padding:4px 0';
vRow.textContent = `PopLang v${API.version}`;
box.appendChild(vRow);
ov.onclick = e => { if (e.target === ov) ov.remove(); };
ov.appendChild(box);
document.body.appendChild(ov);
}
// ─── CUSTOMIZE PANELİ ──────────────────────────────────────────────────────────
function _openCustomize() {
document.getElementById('ppl-cust-ov')?.remove();
const hasCustom = Object.keys(_custom).length > 0;
const ov = document.createElement('div');
ov.id = 'ppl-cust-ov';
ov.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:100001;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box';
const box = document.createElement('div');
box.style.cssText = 'background:#fff;border-radius:14px;padding:20px;width:100%;max-width:480px;max-height:92vh;overflow-y:auto;box-sizing:border-box;font-family:inherit';
const mkHr = () => { const hr = document.createElement('hr'); hr.style.cssText = 'border:none;border-top:1px solid #eee;margin:14px 0'; return hr; };
// Başlık
const hdr = document.createElement('div');
hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:14px';
const titleEl = document.createElement('span');
titleEl.textContent = _ui('custTitle');
titleEl.style.cssText = 'font-weight:700;font-size:15px';
const xBtn = mkB('✕', () => ov.remove());
xBtn.style.cssText = 'background:none;border:none;font-size:20px;cursor:pointer;color:#aaa;padding:0';
hdr.append(titleEl, xBtn);
box.appendChild(hdr);
// Bilgi kutusu
const info = document.createElement('div');
info.style.cssText = 'background:#e8f4fd;border-radius:8px;padding:12px;margin-bottom:14px;font-size:12px;line-height:1.7;color:#1a5276';
const steps = [
`1. <strong>${_ui('custStep1')}</strong>`,
`2. <a href="https://gemini.google.com" target="_blank" style="color:#1a5276;font-weight:600">Google Gemini</a>'ye yapıştır — ${_ui('custStep2').replace('Google Gemini\'ye yapıştır, ','')}`,
`3. ${_ui('custStep3')}`,
`4. ${_ui('custStep4')}`,
];
info.innerHTML = `<div style="font-weight:600;margin-bottom:6px">${_ui('custInfo')}</div>` + steps.map(s => `<div style="margin-left:8px;margin-bottom:3px">${s}</div>`).join('');
box.appendChild(info);
// Export
const expLabel = document.createElement('div');
expLabel.textContent = _ui('exportLabel');
expLabel.style.cssText = 'font-weight:700;font-size:13px;margin-bottom:6px';
box.appendChild(expLabel);
const expBtn = mkB(_ui('exportBtn'), () => {
const exported = {};
_SCRIPT_IDS.forEach(id => {
const tr = _S[id]?.TR;
if (tr && Object.keys(tr).length) exported[id] = tr;
});
if (!Object.keys(exported).length) {
expBtn.textContent = _ui('exportEmpty');
setTimeout(() => expBtn.textContent = _ui('exportBtn'), 2500);
return;
}
const prompt = [
'Translate the JSON values below to the target language.',
'Rules: keep all emojis, {n}, %s and \\n exactly as-is.',
'Return ONLY valid JSON, no markdown, no explanation.',
'',
'Target language: [ENTER TARGET LANGUAGE HERE]',
'',
JSON.stringify(exported, null, 2),
].join('\n');
navigator.clipboard.writeText(prompt);
expBtn.textContent = _ui('exportCopied');
setTimeout(() => expBtn.textContent = _ui('exportBtn'), 2500);
});
expBtn.style.cssText = 'width:100%;padding:10px;background:#0d6efd;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit';
box.appendChild(expBtn);
box.appendChild(mkHr());
// Import
const impLabel = document.createElement('div');
impLabel.textContent = _ui('importLabel');
impLabel.style.cssText = 'font-weight:700;font-size:13px;margin-bottom:6px';
box.appendChild(impLabel);
const ta = document.createElement('textarea');
ta.placeholder = _ui('importPH');
ta.style.cssText = 'width:100%;height:150px;font-size:11px;font-family:monospace;padding:8px;border:1px solid #ddd;border-radius:6px;box-sizing:border-box;resize:vertical;margin-bottom:8px';
box.appendChild(ta);
const errDiv = document.createElement('div');
errDiv.style.cssText = 'font-size:12px;min-height:18px;margin-bottom:8px';
box.appendChild(errDiv);
const applyBtn = mkB(_ui('importApply'), () => {
try {
const raw = ta.value.trim();
if (!raw) { errDiv.style.color = '#dc3545'; errDiv.textContent = _ui('importPasteFirst'); return; }
const data = JSON.parse(raw);
let applied = 0;
_SCRIPT_IDS.forEach(id => {
if (data[id] && typeof data[id] === 'object') {
localStorage.setItem(_lsKey(id), JSON.stringify(data[id]));
Object.assign(_custom, { [id]: data[id] }); // runtime güncelle
applied++;
}
});
if (!applied) { errDiv.style.color = '#dc3545'; errDiv.textContent = _ui('importNoMatch'); return; }
errDiv.style.color = '#198754';
errDiv.textContent = _ui('importApplied').replace('{n}', applied);
setTimeout(() => { ov.remove(); location.reload(); }, 1200);
} catch (err) {
errDiv.style.color = '#dc3545';
errDiv.textContent = _ui('importError') + err.message;
}
});
applyBtn.style.cssText = 'width:100%;padding:10px;background:#198754;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;font-family:inherit';
box.appendChild(applyBtn);
// Reset (sadece custom varsa göster)
if (hasCustom) {
box.appendChild(mkHr());
const resetBtn = mkB(_ui('resetBtn'), () => {
if (!confirm(_ui('resetConfirm'))) return;
_SCRIPT_IDS.forEach(id => localStorage.removeItem(_lsKey(id)));
ov.remove();
location.reload();
});
resetBtn.style.cssText = 'width:100%;padding:8px;background:none;color:#dc3545;border:1px solid #dc3545;border-radius:8px;cursor:pointer;font-size:12px;font-family:inherit';
box.appendChild(resetBtn);
}
ov.onclick = e => { if (e.target === ov) ov.remove(); };
ov.appendChild(box);
document.body.appendChild(ov);
}
// ═══════════════════════════════════════════════════════════════════════════════
// TOP VIP INJECTION — karakter sayfasında "🌍 Dil Ayarları" menü öğesi
// ═══════════════════════════════════════════════════════════════════════════════
function _injectTopVip() {
if (!location.href.includes('/World/Popmundo.aspx/Character')) return;
if (document.getElementById('mnu-ppl-lang')) return;
const item = { id: 'mnu-ppl-lang', label: _ui('menuLabel'), fn: () => API.openUI() };
// PopControl MenuManager varsa ona devret
const pc = (typeof unsafeWindow !== 'undefined' && unsafeWindow.PopControl) || window.PopControl;
if (pc?.MenuManager) {
pc.MenuManager.registerMenu({
id: 'top-vip',
title: '⭐ TOP VIP ⭐',
position: 'above-career',
items: [item],
collapsible: true,
});
return;
}
// Standalone injection (PopControl olmadan)
let ul = document.querySelector('#top-vip-menu ul');
if (!ul) {
const ref = [...document.querySelectorAll('.menu h3')]
.find(h => /Kariyer|Career|Carreira/i.test(h.textContent))?.closest('.menu');
if (!ref) return; // menü hazır değil, retry bekleniyor
const isCol = localStorage.getItem('top-vip-collapsed') === 'true';
const h3 = document.createElement('h3');
h3.textContent = '⭐ TOP VIP ⭐';
h3.style.cssText = 'background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;text-align:center;padding:8px;margin:0;border-radius:6px 6px 0 0;cursor:pointer;box-shadow:0 2px 8px rgba(102,126,234,.3);user-select:none';
ul = document.createElement('ul');
ul.style.cssText = `margin:0;padding:8px 0;background:#f8f9fa;border:1px solid #e9ecef;border-top:none;border-radius:0 0 6px 6px;${isCol ? 'display:none;' : ''}`;
h3.onclick = () => {
const collapsed = ul.style.display === 'none';
ul.style.display = collapsed ? '' : 'none';
localStorage.setItem('top-vip-collapsed', String(!collapsed));
};
const menu = document.createElement('div');
menu.id = 'top-vip-menu';
menu.className = 'menu';
menu.append(h3, ul);
ref.before(menu);
menu.after(Object.assign(document.createElement('div'), { style: 'height:12px' }));
}
if (document.getElementById(item.id)) return;
const a = document.createElement('a');
a.href = '#';
a.id = item.id;
a.textContent = item.label;
a.style.cssText = 'color:#667eea;font-weight:600;text-decoration:none;display:block;padding:4px 12px;border-radius:4px;transition:all .2s';
a.onmouseover = () => { a.style.background = '#667eea'; a.style.color = '#fff'; };
a.onmouseout = () => { a.style.background = ''; a.style.color = '#667eea'; };
a.onclick = e => { e.preventDefault(); item.fn(); };
const li = document.createElement('li');
li.style.margin = '2px 0';
li.appendChild(a);
ul.appendChild(li);
}
// ═══════════════════════════════════════════════════════════════════════════════
// POPCONTROL ENTEGRASYONU
// PopControl settings panelindeki Dil bölümüne "🌍 Dil Ayarları" butonu enjekte
// eder. PopControl settings paneli her açıldığında yeniden render edildiğinden
// MutationObserver ile izliyoruz.
// ═══════════════════════════════════════════════════════════════════════════════
function _patchPopControl() {
// PopControl'ün settings overlay'ini izle
const obs = new MutationObserver(() => {
const box = document.querySelector('#ppc-ov > div');
if (!box) return;
if (box.dataset.pplPatched) return;
box.dataset.pplPatched = '1';
// "Dil" başlığını bul (STR.langLabel içeren div)
const langSection = [...box.querySelectorAll('div')]
.find(el => el.textContent.trim() === 'Dil' || el.textContent.trim() === 'Language' || el.textContent.trim() === 'Idioma');
if (!langSection) return;
// Mevcut dil pill row'unu bul (hemen altındaki sibling)
const pillRow = langSection.nextElementSibling;
if (!pillRow) return;
// Pill row'daki "Customize" butonunu PopLang'e yönlendir
pillRow.querySelectorAll('button').forEach(btn => {
const txt = btn.textContent.trim();
// Customize butonunu PopLang'e yönlendir
if (txt.includes('Customize') || txt.includes('🌍')) {
btn.onclick = () => { document.getElementById('ppc-ov')?.remove(); API.openUI(); };
}
// Dil butonlarını PopLang üzerinden uygula (cookie + reload zaten yapıyor, dokunmaya gerek yok)
});
});
obs.observe(document.body, { childList: true, subtree: true });
}
// ═══════════════════════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════════════════════
const _domReady = fn => {
if (document.readyState !== 'loading') fn();
else document.addEventListener('DOMContentLoaded', fn, { once: true });
};
// Karakter menüsüne inject — retry mekanizması ile
let _injRetry = 0;
function _tryInject() {
_injectTopVip();
// Menü henüz hazır değilse kısa süre içinde tekrar dene
if (!document.getElementById('mnu-ppl-lang') && _injRetry < 20) {
_injRetry++;
setTimeout(_tryInject, 300);
}
}
_domReady(() => {
// PopControl entegrasyonu
_patchPopControl();
// PopControlReady event'i dinle (PopControl yüklüyse MenuManager'a inject)
document.addEventListener('PopControlReady', () => {
setTimeout(_tryInject, 100);
}, { once: true });
// PopControl yoksa veya daha yavaşsa direkt inject dene
_tryInject();
// SPA gezintisi — karakter sayfasına her gidişte tekrar inject et
let _lastUrl = location.href;
new MutationObserver(() => {
if (location.href === _lastUrl) return;
_lastUrl = location.href;
_injRetry = 0;
setTimeout(_tryInject, 400);
}).observe(document.body, { childList: true, subtree: false });
});
console.log(`[PopLang] v${API.version} hazır — dil: ${_lang}`);
})();