Move translations to server

This commit is contained in:
Deluan 2020-05-01 18:29:50 -04:00 committed by Deluan Quintão
parent 1a9663d432
commit 41cf99541d
19 changed files with 954 additions and 801 deletions

2
go.mod
View File

@ -40,5 +40,5 @@ require (
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/djherbis/atime.v1 v1.0.0 // indirect
gopkg.in/djherbis/stream.v1 v1.3.1 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v2 v2.2.8
)

View File

@ -1 +1 @@
-s -r "(\.go$$|navidrome.toml)" -R "(Jamstash-master|^ui|^data)" -- go run .
-s -r "(\.go$$|navidrome.toml|resources)" -R "(Jamstash-master|^ui|^data)" -- go run .

258
resources/i18n/de.json Normal file
View File

@ -0,0 +1,258 @@
{
"languageName": "Deutsch",
"resources": {
"song": {
"name": "Song |||| Songs",
"fields": {
"albumArtist": "Album Künstler",
"duration": "Dauer",
"trackNumber": "Titel #",
"playCount": "Aufrufe",
"title": "Titel",
"artist": "Künstler",
"album": "Album",
"path": "Dateipfad",
"genre": "Genre",
"compilation": "Kompilation",
"year": "Jahr",
"size": "Dateigröße",
"updatedAt": "Hochgeladen um"
},
"actions": {
"addToQueue": "Später abspielen"
},
"action": {
"playNow": "Jetzt abspielen"
}
},
"album": {
"name": "Album |||| Alben",
"fields": {
"albumArtist": "Album Künstler",
"artist": "Künstler",
"duration": "Dauer",
"songCount": "Songs",
"playCount": "Aufrufe",
"name": "Name",
"genre": "Genre",
"compilation": "Kompilation",
"year": "Jahr"
},
"actions": {
"playAll": "Abspielen",
"playNext": "Als nächstes abspielen",
"addToQueue": "Später abspielen",
"shuffle": "Zufallswiedergabe"
}
},
"artist": {
"name": "Künstler |||| Künstler",
"fields": {
"name": "Name",
"albumCount": "Albumanzahl"
}
},
"user": {
"name": "Nutzer |||| Nutzer",
"fields": {
"userName": "Nutzername",
"isAdmin": "Ist Admin",
"lastLoginAt": "Letzer Login um ",
"updatedAt": "Aktualisiert am",
"name": "Name"
}
},
"player": {
"name": "Player |||| Players",
"fields": {
"name": "Name",
"transcodingId": "Transkodierungs-ID",
"maxBitRate": "Max. Bitrate",
"client": "Client",
"userName": "Nutzername",
"lastSeen": "Zuletzt gesehen um"
}
},
"transcoding": {
"name": "Transcodierung |||| Transcodierungen",
"fields": {
"name": "Name",
"targetFormat": "Zielformat",
"defaultBitRate": "Standardbitrate",
"command": "Befehl"
}
}
},
"ra": {
"auth": {
"welcome1": "Vielen Dank für die Installation von Navidrome!",
"welcome2": "Als erstes erstelle einen Admin-Benutzer",
"confirmPassword": "Passwort bestätigen",
"buttonCreateAdmin": "Admin erstellen",
"auth_check_error": "Bitte einloggen um fortzufahren",
"user_menu": "Profil",
"username": "Nutzername",
"password": "Passwort",
"sign_in": "Anmelden",
"sign_in_error": "Fehler bei der Anmeldung",
"logout": "Abmelden"
},
"validation": {
"invalidChars": "Bitte nur Buchstaben und Zahlen verwenden",
"passwordDoesNotMatch": "Passwort stimmt nicht überein",
"required": "Benötigt",
"minLength": "Muss mindestens %{min} Zeichen lang sein",
"maxLength": "Darf maximal %{max} Zeichen lang sein",
"minValue": "Muss mindestens %{min} sein",
"maxValue": "Muss %{max} oder weniger sein",
"number": "Muss eine Nummer sein",
"email": "Muss eine gültige E-Mail sein",
"oneOf": "Es muss einer sein von: %{options}",
"regex": "Es muss folgendem regulären Ausdruck entsprechen: %{pattern}"
},
"action": {
"add_filter": "Filter hinzufügen",
"add": "Neu",
"back": "Zurück",
"bulk_actions": "Ein Element ausgewählt |||| %{smart_count} Elemente ausgewählt",
"cancel": "Abbrechen",
"clear_input_value": "Eingabe löschen",
"clone": "Klonen",
"confirm": "Bestätigen",
"create": "Erstellen",
"delete": "Löschen",
"edit": "Bearbeiten",
"export": "Exportieren",
"list": "Liste",
"refresh": "Aktualisieren",
"remove_filter": "Filter entfernen",
"remove": "Entfernen",
"save": "Speichern",
"search": "Suchen",
"show": "Anzeigen",
"sort": "Sortieren",
"undo": "Zurücksetzen",
"expand": "Expandieren",
"close": "Schließen",
"open_menu": "Menü öffnen",
"close_menu": "Menü schließen"
},
"boolean": {
"true": "Ja",
"false": "Nein"
},
"page": {
"create": "%{name} erstellen",
"dashboard": "Dashboard",
"edit": "%{name} #%{id}",
"error": "Etwas ist schief gelaufen",
"list": "%{name}",
"loading": "Laden",
"not_found": "Nicht gefunden",
"show": "%{name} #%{id}",
"empty": "Noch kein %{name}.\n",
"invite": "Möchten du eine hinzufügen?"
},
"input": {
"file": {
"upload_several": "Zum Hochladen Dateien hineinziehen oder hier klicken, um Dateien auszuwählen.",
"upload_single": "Zum Hochladen Datei hineinziehen oder hier klicken, um eine Datei auszuwählen."
},
"image": {
"upload_several": "Zum Hochladen Bilder hineinziehen oder hier klicken, um Bilder auszuwählen.",
"upload_single": "Zum Hochladen Bild hineinziehen oder hier klicken, um ein Bild auszuwählen."
},
"references": {
"all_missing": "Die zugehörigen Referenzen konnten nicht gefunden werden.",
"many_missing": "Mindestens eine der zugehörigen Referenzen scheint nicht mehr verfügbar zu sein.",
"single_missing": "Eine zugehörige Referenz scheint nicht mehr verfügbar zu sein."
},
"password": {
"toggle_visible": "Passwort verbergen",
"toggle_hidden": "Passwort anzeigen"
}
},
"message": {
"about": "Über",
"are_you_sure": "Bist du sicher?",
"bulk_delete_content": "Möchtest du \"%{name}\" wirklich löschen? |||| Möchtest du diese %{smart_count} Elemente wirklich löschen?",
"bulk_delete_title": "Lösche %{name} |||| Lösche %{smart_count} %{name} Elemente",
"delete_content": "Möchtest du diesen Inhalt wirklich löschen?",
"delete_title": "Lösche %{name} #%{id}",
"details": "Details",
"error": "Ein Fehler ist aufgetreten und ihre Anfrage konnte nicht abgeschlossen werden.",
"invalid_form": "Das Formular ist ungültig. Bitte überprüfe deine Eingaben.",
"loading": "Die Seite wird geladen.",
"no": "Nein",
"not_found": "Die Seite konnte nicht gefunden werden.",
"yes": "Ja",
"unsaved_changes": "Einige deiner Änderungen wurden nicht gespeichert. Bist du sicher, dass du sie ignorieren möchtest?"
},
"navigation": {
"no_results": "Keine Resultate gefunden",
"no_more_results": "Die Seite %{page} enthält keine Inhalte.",
"page_out_of_boundaries": "Die Seite %{page} liegt ausserhalb des gültigen Bereichs",
"page_out_from_end": "Letzte Seite",
"page_out_from_begin": "Erste Seite",
"page_range_info": "%{offsetBegin}-%{offsetEnd} von %{total}",
"page_rows_per_page": "Zeilen pro Seite:",
"next": "Weiter",
"prev": "Zurück"
},
"notification": {
"updated": "Element wurde aktualisiert |||| %{smart_count} Elemente wurden aktualisiert",
"created": "Element wurde erstellt",
"deleted": "Element wurde gelöscht |||| %{smart_count} Elemente wurden gelöscht",
"bad_item": "Fehlerhaftes Elemente",
"item_doesnt_exist": "Das Element existiert nicht",
"http_error": "Fehler beim Kommunizieren mit dem Server",
"data_provider_error": "dataProvider Fehler. Prüfe die Konsole für Details.",
"i18n_error": "Die Übersetzungen für die angegebene Sprache können nicht geladen werden",
"canceled": "Aktion abgebrochen",
"logged_out": "Ihr Session wurde beendet. Bitte erneut verbinden."
}
},
"message": {
"note": "Hinweis",
"transcodingDisabled": "Die Änderung der Transcodierungskonfiguration über die Webschnittstelle ist aus Sicherheitsgründen deaktiviert. Wenn du die Transcodierungsoptionen ändern (bearbeiten oder hinzufügen) möchtest, starte den Server mit der Konfigurationsoption %{config} neu.",
"transcodingEnabled": "Navidrome läuft derzeit mit %{config}, wodurch es möglich ist, Systembefehle aus den Transkodierungseinstellungen über die Webschnittstelle auszuführen. Wir empfehlen, es aus Sicherheitsgründen zu deaktivieren und nur bei der Konfiguration von Transkodierungsoptionen zu aktivieren."
},
"menu": {
"library": "Bibliothek",
"settings": "Einstellungen",
"version": "Version %{version}",
"theme": "Design",
"personal": {
"name": "Persönlich",
"options": {
"theme": "Thema",
"language": "Sprache"
}
}
},
"player": {
"playListsText": "Warteschlange abspielen",
"openText": "Öffnen",
"closeText": "Schließen",
"notContentText": "Keine Musik",
"clickToPlayText": "Anklicken um abzuspielen",
"clickToPauseText": "Zum Pausieren anklicken",
"nextTrackText": "Nächster Titel",
"previousTrackText": "Vorheriger Titel",
"reloadText": "Neu laden",
"volumeText": "Lautstärke",
"toggleLyricText": "Liedtext umschalten",
"toggleMiniModeText": "Minimieren",
"destroyText": "Zerstören",
"downloadText": "Herunterladen",
"removeAudioListsText": "Audiolisten löschen",
"clickToDeleteText": "Klicken um %{Name} zu Löschen",
"emptyLyricText": "Kein Liedtext",
"playModeText": {
"order": "Der Reihe nach",
"orderLoop": "Wiederholen",
"singleLoop": "Eins wiederholen",
"shufflePlay": "Zufallswiedergabe"
}
}
}

258
resources/i18n/pt.json Normal file
View File

@ -0,0 +1,258 @@
{
"languageName": "Português",
"resources": {
"song": {
"name": "Música |||| Músicas",
"fields": {
"albumArtist": "",
"duration": "Duração",
"trackNumber": "#",
"playCount": "Execuções",
"title": "Título",
"artist": "Artista",
"album": "Álbum",
"path": "Arquivo",
"genre": "Gênero",
"compilation": "Coletânea",
"year": "Ano",
"size": "Tamanho",
"updatedAt": "Últ. Atualização"
},
"actions": {
"addToQueue": "Tocar por último"
},
"action": {
"playNow": ""
}
},
"album": {
"name": "Álbum |||| Álbuns",
"fields": {
"albumArtist": "",
"artist": "Artista",
"duration": "Duração",
"songCount": "Músicas",
"playCount": "Execuções",
"name": "Nome",
"genre": "Gênero",
"compilation": "Coletânea",
"year": "Ano"
},
"actions": {
"playAll": "Play",
"playNext": "Play Next",
"addToQueue": "Tocar por último",
"shuffle": "Shuffle"
}
},
"artist": {
"name": "Artista |||| Artistas",
"fields": {
"name": "Nome",
"albumCount": "Total de Álbuns"
}
},
"user": {
"name": "Usuário |||| Usuários",
"fields": {
"userName": "Usuário",
"isAdmin": "Admin?",
"lastLoginAt": "Últ. Login",
"updatedAt": "Últ. Atualização",
"name": "Nome"
}
},
"player": {
"name": "Tocador |||| Tocadores",
"fields": {
"name": "Nome",
"transcodingId": "Conversão",
"maxBitRate": "Bitrate máx",
"client": "Cliente",
"userName": "Usuário",
"lastSeen": "Últ. acesso"
}
},
"transcoding": {
"name": "Conversão |||| Conversões",
"fields": {
"name": "Nome",
"targetFormat": "Formato",
"defaultBitRate": "Bitrate padrão",
"command": "Comando"
}
}
},
"ra": {
"auth": {
"welcome1": "Obrigado por instalar Navidrome!",
"welcome2": "Para iniciar, crie um usuário admin",
"confirmPassword": "Confirme a senha",
"buttonCreateAdmin": "Criar Admin",
"auth_check_error": "Por favor, faça login para continuar",
"user_menu": "Perfil",
"username": "Usuário",
"password": "Senha",
"sign_in": "Entrar",
"sign_in_error": "Erro na autenticação, tente novamente.",
"logout": "Sair"
},
"validation": {
"invalidChars": "Somente use letras e numeros",
"passwordDoesNotMatch": "Senha não confere",
"required": "Obrigatório",
"minLength": "Deve ser ter no mínimo %{min} caracteres",
"maxLength": "Deve ter no máximo %{max} caracteres",
"minValue": "Deve ser %{min} ou maior",
"maxValue": "Deve ser %{max} ou menor",
"number": "Deve ser um número",
"email": "Deve ser um email válido",
"oneOf": "Deve ser uma das seguintes opções: %{options}",
"regex": "Deve ter o formato específico (regexp): %{pattern}"
},
"action": {
"add_filter": "Adicionar Filtro",
"add": "Adicionar",
"back": "Voltar",
"bulk_actions": "1 item selecionado |||| %{smart_count} itens selecionados",
"cancel": "Cancelar",
"clear_input_value": "Limpar campo",
"clone": "Duplicar",
"confirm": "Confirmar",
"create": "Novo",
"delete": "Deletar",
"edit": "Editar",
"export": "Exportar",
"list": "Listar",
"refresh": "Atualizar",
"remove_filter": "Cancelar filtro",
"remove": "Excluir",
"save": "Salvar",
"search": "Buscar",
"show": "Exibir",
"sort": "Ordenar",
"undo": "Desfazer",
"expand": "Expandir",
"close": "Fechar",
"open_menu": "",
"close_menu": ""
},
"boolean": {
"true": "Sim",
"false": "Não"
},
"page": {
"create": "Novo %{name}",
"dashboard": "Painel de Controle",
"edit": "%{name} #%{id}",
"error": "Um erro ocorreu",
"list": "Listar %{name}",
"loading": "Carregando",
"not_found": "Não encontrado",
"show": "%{name} #%{id}",
"empty": "",
"invite": ""
},
"input": {
"file": {
"upload_several": "Arraste alguns arquivos para fazer o upload, ou clique para selecioná-los.",
"upload_single": "Arraste o arquivo para fazer o upload, ou clique para selecioná-lo."
},
"image": {
"upload_several": "Arraste algumas imagens para fazer o upload ou clique para selecioná-las",
"upload_single": "Arraste um arquivo para upload ou clique em selecionar arquivo."
},
"references": {
"all_missing": "Não foi possível encontrar os dados das referencias.",
"many_missing": "Pelo menos uma das referências passadas não está mais disponível.",
"single_missing": "A referência passada aparenta não estar mais disponível."
},
"password": {
"toggle_visible": "",
"toggle_hidden": ""
}
},
"message": {
"about": "Sobre",
"are_you_sure": "Tem certeza?",
"bulk_delete_content": "Você tem certeza que deseja excluir %{name}? |||| Você tem certeza que deseja excluir estes %{smart_count} itens?",
"bulk_delete_title": "Excluir %{name} |||| Excluir %{smart_count} %{name} itens",
"delete_content": "Você tem certeza que deseja excluir?",
"delete_title": "Excluir %{name} #%{id}",
"details": "Detalhes",
"error": "Um erro ocorreu e a sua requisição não pôde ser completada.",
"invalid_form": "Este formulário não está valido. Certifique-se de corrigir os erros",
"loading": "A página está carregando. Um momento, por favor",
"no": "Não",
"not_found": "Foi digitada uma URL inválida, ou o link pode estar quebrado.",
"yes": "Sim",
"unsaved_changes": ""
},
"navigation": {
"no_results": "Nenhum resultado encontrado",
"no_more_results": "A página numero %{page} está fora dos limites. Tente a página anterior.",
"page_out_of_boundaries": "Página %{page} fora o limite",
"page_out_from_end": "Não é possível ir após a última página",
"page_out_from_begin": "Não é possível ir antes da primeira página",
"page_range_info": "%{offsetBegin}-%{offsetEnd} de %{total}",
"page_rows_per_page": "Resultados por página:",
"next": "Próximo",
"prev": "Anterior"
},
"notification": {
"updated": "Item atualizado com sucesso |||| %{smart_count} itens foram atualizados com sucesso",
"created": "Item criado com sucesso",
"deleted": "Item removido com sucesso! |||| %{smart_count} itens foram removidos com sucesso",
"bad_item": "Item incorreto",
"item_doesnt_exist": "Esse item não existe mais",
"http_error": "Erro na comunicação com servidor",
"data_provider_error": "Erro interno do servidor. Entre em contato",
"i18n_error": "Não foi possível carregar as traduções para o idioma especificado",
"canceled": "Ação cancelada",
"logged_out": "Sua sessão foi encerrada. Por favor, reconecte"
}
},
"message": {
"note": "",
"transcodingDisabled": "",
"transcodingEnabled": ""
},
"menu": {
"library": "Biblioteca",
"settings": "Configurações",
"version": "Versão %{version}",
"theme": "",
"personal": {
"name": "Pessoal",
"options": {
"theme": "Tema",
"language": "Língua"
}
}
},
"player": {
"playListsText": "Fila de Execução",
"openText": "Abrir",
"closeText": "Fechar",
"notContentText": "",
"clickToPlayText": "Clique para tocar",
"clickToPauseText": "Clique para pausar",
"nextTrackText": "Próxima faixa",
"previousTrackText": "Faixa anterior",
"reloadText": "",
"volumeText": "Volume",
"toggleLyricText": "",
"toggleMiniModeText": "Minimizar",
"destroyText": "",
"downloadText": "",
"removeAudioListsText": "Limpar fila de execução",
"clickToDeleteText": "Clique para remover %{name}",
"emptyLyricText": "",
"playModeText": {
"order": "Em ordem",
"orderLoop": "Repetir tudo",
"singleLoop": "Repetir",
"shufflePlay": "Aleatório"
}
}
}

View File

@ -48,6 +48,7 @@ func (app *Router) routes(path string) http.Handler {
app.R(r, "/artist", model.Artist{}, true)
app.R(r, "/player", model.Player{}, true)
app.R(r, "/transcoding", model.Transcoding{}, conf.Server.EnableTranscodingConfig)
app.addResource(r, "/translation", newTranslationRepository, false)
// Keepalive endpoint to be used to keep the session valid (ex: while playing songs)
r.Get("/keepalive/*", func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"response":"ok"}`)) })
@ -64,12 +65,16 @@ func (app *Router) R(r chi.Router, pathPrefix string, model interface{}, persist
constructor := func(ctx context.Context) rest.Repository {
return app.ds.Resource(ctx, model)
}
app.addResource(r, pathPrefix, constructor, persistable)
}
func (app *Router) addResource(r chi.Router, pathPrefix string, constructor rest.RepositoryConstructor, persistable bool) {
r.Route(pathPrefix, func(r chi.Router) {
r.Get("/", rest.GetAll(constructor))
if persistable {
r.Post("/", rest.Post(constructor))
}
r.Route("/{id:[0-9a-f\\-]+}", func(r chi.Router) {
r.Route("/{id}", func(r chi.Router) {
r.Use(UrlParams)
r.Get("/", rest.Get(constructor))
if persistable {

View File

@ -13,7 +13,7 @@ import (
"github.com/deluan/navidrome/model"
)
// Injects the `firstTime` config in the `index.html` template
// Injects the config in the `index.html` template
func ServeIndex(ds model.DataStore, fs http.FileSystem) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
c, err := ds.User(r.Context()).CountAll()
@ -21,6 +21,9 @@ func ServeIndex(ds model.DataStore, fs http.FileSystem) http.HandlerFunc {
t := getIndexTemplate(r, fs)
if err != nil {
log.Error("Error loading default English translation file", err)
}
appConfig := map[string]interface{}{
"version": consts.Version(),
"firstTime": firstTime,

114
server/app/translations.go Normal file
View File

@ -0,0 +1,114 @@
package app
import (
"context"
"encoding/json"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/resources"
"github.com/deluan/rest"
)
const i18nFolder = "i18n"
type translation struct {
ID string `json:"id"`
Name string `json:"name"`
Data string `json:"data"`
}
var (
once sync.Once
translations map[string]translation
)
func newTranslationRepository(context.Context) rest.Repository {
if err := loadTranslations(); err != nil {
log.Error("Error loading translation files", err)
}
return &translationRepository{}
}
type translationRepository struct{}
func (r *translationRepository) Read(id string) (interface{}, error) {
if t, ok := translations[id]; ok {
return t, nil
}
return nil, rest.ErrNotFound
}
// Simple Count implementation. Does not support any `options`
func (r *translationRepository) Count(options ...rest.QueryOptions) (int64, error) {
return int64(len(translations)), nil
}
// Simple ReadAll implementation, only returns IDs. Does not support any `options`, always sort by name asc
func (r *translationRepository) ReadAll(options ...rest.QueryOptions) (interface{}, error) {
var result []translation
for _, t := range translations {
t.Data = ""
result = append(result, t)
}
sort.Slice(result, func(i, j int) bool {
return result[i].Name < result[j].Name
})
return result, nil
}
func (r *translationRepository) EntityName() string {
return "translation"
}
func (r *translationRepository) NewInstance() interface{} {
return &translation{}
}
func loadTranslations() (loadError error) {
once.Do(func() {
translations = make(map[string]translation)
dir, err := resources.AssetFile().Open(i18nFolder)
if err != nil {
loadError = err
return
}
files, err := dir.Readdir(0)
if err != nil {
loadError = err
return
}
for _, f := range files {
t, err := loadTranslation(f.Name())
if err != nil {
log.Error("Error loading translation file", "file", f.Name(), err)
continue
}
translations[t.ID] = t
}
})
return
}
func loadTranslation(fileName string) (trans translation, err error) {
id := strings.TrimSuffix(fileName, filepath.Ext(fileName))
filePath := filepath.Join(i18nFolder, fileName)
data, err := resources.Asset(filePath)
trans.Data = string(data)
if err != nil {
return
}
var out map[string]interface{}
err = json.Unmarshal(data, &out)
if err != nil {
return
}
trans.Name = out["languageName"].(string)
trans.ID = id
return
}
var _ rest.Repository = (*translationRepository)(nil)

View File

@ -4,8 +4,6 @@ import { createHashHistory } from 'history'
import { Admin, Resource } from 'react-admin'
import dataProvider from './dataProvider'
import authProvider from './authProvider'
import polyglotI18nProvider from 'ra-i18n-polyglot'
import messages from './i18n'
import { Layout, Login } from './layout'
import transcoding from './transcoding'
import player from './player'
@ -18,11 +16,7 @@ import { albumViewReducer } from './album/albumState'
import customRoutes from './routes'
import themeReducer from './personal/themeReducer'
import createAdminStore from './store/createAdminStore'
const i18nProvider = polyglotI18nProvider(
(locale) => (messages[locale] ? messages[locale] : messages.en),
localStorage.getItem('locale') || 'en'
)
import i18nProvider from './i18nProvider'
const history = createHashHistory()
@ -52,7 +46,6 @@ const App = () => (
<Resource name="artist" {...artist} options={{ subMenu: 'library' }} />,
<Resource name="album" {...album} options={{ subMenu: 'library' }} />,
<Resource name="song" {...song} options={{ subMenu: 'library' }} />,
<Resource name="albumSong" />,
permissions === 'admin' ? (
<Resource name="user" {...user} options={{ subMenu: 'settings' }} />
) : null,
@ -70,6 +63,8 @@ const App = () => (
) : (
<Resource name="transcoding" />
),
<Resource name="albumSong" />,
<Resource name="translation" />,
<Player />,
]}
</Admin>

View File

@ -47,7 +47,6 @@ const Player = () => {
destroyText: translate('player.destroyText'),
downloadText: translate('player.downloadText'),
removeAudioListsText: translate('player.removeAudioListsText'),
controllerTitle: translate('player.controllerTitle'),
clickToDeleteText: (name) =>
translate('player.clickToDeleteText', { name }),
emptyLyricText: translate('player.emptyLyricText'),

View File

@ -1,148 +0,0 @@
import deepmerge from 'deepmerge'
import englishMessages from 'ra-language-english'
export default deepmerge(englishMessages, {
languageName: 'English',
resources: {
song: {
name: 'Song |||| Songs',
fields: {
title: 'Title',
artist: 'Artist',
album: 'Album',
path: 'File Path',
genre: 'Genre',
compilation: 'Compilation',
duration: 'Time',
year: 'Year',
trackNumber: '#',
playCount: 'Plays',
size: 'File Size',
updatedAt: 'Updated At',
},
actions: {
playNow: 'Play Now',
addToQueue: 'Play Later',
},
},
album: {
name: 'Album |||| Albums',
fields: {
albumArtist: 'Album Artist',
name: 'Name',
artist: 'Artist',
songCount: 'Songs',
playCount: 'Plays',
genre: 'Genre',
compilation: 'Compilation',
duration: 'Duration',
year: 'Year',
},
actions: {
playAll: 'Play Now',
playNext: 'Play Next',
addToQueue: 'Play Later',
shuffle: 'Shuffle',
},
},
artist: {
name: 'Artist |||| Artists',
fields: {
name: 'Nome',
albumCount: 'Album Count',
},
},
user: {
name: 'User |||| Users',
fields: {
userName: 'Username',
name: 'Name',
isAdmin: 'Is Admin?',
lastLoginAt: 'Last Login',
updatedAt: 'Updated At',
},
},
player: {
name: 'Player |||| Players',
fields: {
name: 'Name',
transcodingId: 'Transcoding',
maxBitRate: 'Max BitRate',
client: 'Client',
userName: 'Username',
lastSeen: 'Last Seen',
},
},
transcoding: {
name: 'Transcoding |||| Transcodings',
fields: {
name: 'Name',
targetFormat: 'Target Format',
defaultBitRate: 'Default BitRate',
command: 'Command',
},
},
},
ra: {
auth: {
welcome1: 'Thanks for installing Navidrome!',
welcome2: 'To start, create an admin user',
confirmPassword: 'Confirm Password',
buttonCreateAdmin: 'Create Admin',
},
validation: {
invalidChars: 'Please only use letter and numbers',
passwordDoesNotMatch: 'Password does not match',
},
},
message: {
note: 'NOTE',
transcodingDisabled:
'Changing the transcoding configuration through the web interface is disabled for security ' +
'reasons. If you would like to change (edit or add) transcoding options, restart the server with ' +
'the %{config} configuration option.',
transcodingEnabled:
'Navidrome is currently running with %{config}, making it possible to run system ' +
'commands from the transcoding settings using the web interface. We recommend to disable it for security reasons ' +
'and only enable it when configuring Transcoding options.',
},
menu: {
library: 'Library',
settings: 'Settings',
version: 'Version %{version}',
theme: 'Theme',
personal: {
name: 'Personal',
options: {
theme: 'Theme',
language: 'Language',
},
},
},
player: {
playListsText: 'Play Queue',
openText: 'Open',
closeText: 'Close',
notContentText: 'No music',
clickToPlayText: 'Click to play',
clickToPauseText: 'Click to pause',
nextTrackText: 'Next track',
previousTrackText: 'Previous track',
reloadText: 'Reload',
volumeText: 'Volume',
toggleLyricText: 'Toggle lyric',
toggleMiniModeText: 'Minimize',
destroyText: 'Destroy',
downloadText: 'Download',
removeAudioListsText: 'Delete audio lists',
controllerTitle: '',
clickToDeleteText: `Click to delete %{name}`,
emptyLyricText: 'No lyric',
playModeText: {
order: 'In order',
orderLoop: 'Repeat',
singleLoop: 'Repeat One',
shufflePlay: 'Shuffle',
},
},
})

View File

@ -1,127 +0,0 @@
import deepmerge from 'deepmerge'
import frenchMessages from 'ra-language-french'
export default deepmerge(frenchMessages, {
languageName: 'Français',
resources: {
song: {
name: 'Piste |||| Pistes',
fields: {
title: 'Titre',
artist: 'Artiste',
album: 'Album',
path: 'Chemin',
genre: 'Genre',
compilation: 'Compilation',
duration: 'Durée',
year: 'Année',
playCount: "Nombre d'écoutes",
trackNumber: '#',
size: 'Taille',
updatedAt: 'Mise à jour',
},
actions: {
addToQueue: 'Ajouter à la file',
},
},
album: {
name: 'Album |||| Albums',
fields: {
name: 'Nom',
artist: 'Artiste',
songCount: 'Numéro de piste',
genre: 'Genre',
playCount: "Numbre d'écoutes",
compilation: 'Compilation',
duration: 'Durée',
year: 'Année',
},
actions: {
playAll: 'Lire',
playNext: 'Lire ensuite',
addToQueue: 'Ajouter à la file',
shuffle: 'Mélanger',
},
},
artist: {
name: 'Artiste |||| Artistes',
fields: {
name: 'Nom',
albumCount: "Nombre d'albums",
},
},
user: {
name: 'Utilisateur |||| Utilisateurs',
fields: {
userName: "Nom d'utilisateur",
isAdmin: 'Administrateur',
lastLoginAt: 'Dernière connexion',
updatedAt: 'Dernière mise à jour',
name: 'Nom',
},
},
player: {
name: 'Lecteur |||| Lecteurs',
fields: {
name: 'Nom',
transcodingId: 'Transcodage',
maxBitRate: 'Bitrate maximum',
client: 'Client',
userName: "Nom d'utilisateur",
lastSeen: 'Vu pour la dernière fois',
},
},
transcoding: {
name: 'Conversion |||| Conversions',
fields: {
name: 'Nom',
targetFormat: 'Format',
defaultBitRate: 'Bitrate par défaut',
command: 'Commande',
},
},
},
ra: {
auth: {
welcome1: "Merci d'avoir installé Navidrome !",
welcome2: 'Pour commencer, créez un compte administrateur',
confirmPassword: 'Confirmer votre mot de passe',
buttonCreateAdmin: 'Créer un compte administrateur',
},
validation: {
invalidChars: "Merci d'utiliser uniquement des chiffres et des lettres",
passwordDoesNotMatch: 'Les mots de passes ne correspondent pas',
},
},
menu: {
library: 'Bibliothèque',
settings: 'Paramètres',
version: 'Version%{version}',
personal: {
name: 'Paramètres personel',
options: {
theme: 'Thème',
language: 'Langue',
},
},
},
player: {
playListsText: 'File de lecture',
openText: 'Ouvrir',
closeText: 'Fermer',
clickToPlayText: 'Cliquer pour lire',
clickToPauseText: 'Cliquer pour mettre en pause',
nextTrackText: 'Morceau suivant',
previousTrackText: 'Morceau précédent',
volumeText: 'Volume',
toggleMiniModeText: 'Minimiser',
removeAudioListsText: 'Vider la liste de lecture',
clickToDeleteText: `Cliquer pour supprimer %{name}`,
playModeText: {
order: 'Ordonner',
orderLoop: 'Tout répéter',
singleLoop: 'Repéter',
shufflePlay: 'Aleatoire',
},
},
})

View File

@ -1,22 +0,0 @@
import deepmerge from 'deepmerge'
import en from './en'
import zh from './zh'
import fr from './fr'
import it from './it'
import nl from './nl'
import pt from './pt'
const addLanguages = (lang) => {
Object.keys(lang).forEach((l) => (languages[l] = deepmerge(en, lang[l])))
}
const languages = { en }
// Add new languages to the object below (please keep alphabetic sort)
addLanguages({ fr, it, nl, pt, zh })
// "Hack" to make "albumSongs" resource use the same translations as "song"
Object.keys(languages).forEach(
(k) => (languages[k].resources.albumSong = languages[k].resources.song)
)
export default languages

View File

@ -1,128 +0,0 @@
import deepmerge from 'deepmerge'
import italianMessages from 'ra-language-italian'
export default deepmerge(italianMessages, {
languageName: 'Italiano',
resources: {
song: {
name: 'Traccia |||| Tracce',
fields: {
title: 'Titolo',
artist: 'Artista',
album: 'Album',
path: 'Percorso',
genre: 'Genere',
compilation: 'Compilation',
duration: 'Durata',
year: 'Anno',
playCount: 'Riproduzioni',
trackNumber: '#',
size: 'Dimensioni',
updatedAt: 'Ultimo aggiornamento',
},
actions: {
playNow: 'Riproduci',
addToQueue: 'Aggiungi alla coda',
},
},
album: {
name: 'Album |||| Album',
fields: {
name: 'Nome',
artist: 'Artista',
songCount: 'Tracce',
genre: 'Genere',
playCount: 'Riproduzioni',
compilation: 'Compilation',
duration: 'Durata',
year: 'Anno',
},
actions: {
playAll: 'Riproduci',
playNext: 'Riproduci come successivo',
addToQueue: 'Aggiungi alla coda',
shuffle: 'Riprodici casualmente',
},
},
artist: {
name: 'Artista |||| Artisti',
fields: {
name: 'Nome',
albumCount: 'Album',
},
},
user: {
name: 'Utente |||| Utenti',
fields: {
userName: 'Utente',
isAdmin: 'Amministratore',
lastLoginAt: 'Ultimo accesso',
updatedAt: 'Ultima modifica',
name: 'Nome',
},
},
player: {
name: 'Client |||| Client',
fields: {
name: 'Nome',
transcodingId: 'Transcodifica',
maxBitRate: 'Bitrate massimo',
client: 'Applicazione',
userName: 'Utente',
lastSeen: 'Ultimo acesso',
},
},
transcoding: {
name: 'Transcodifica |||| Transcodifiche',
fields: {
name: 'Nome',
targetFormat: 'Formato',
defaultBitRate: 'Bitrate predefinito',
command: 'Comando',
},
},
},
ra: {
auth: {
welcome1: 'Grazie per aver installato Navidrome!',
welcome2: 'Per iniziare, crea un amministratore',
confirmPassword: 'Conferma la password',
buttonCreateAdmin: 'Crea amministratore',
},
validation: {
invalidChars: 'Per favore usa solo lettere e numeri',
passwordDoesNotMatch: 'Le password non coincidono',
},
},
menu: {
library: 'Libreria',
settings: 'Impostazioni',
version: 'Versione %{version}',
personal: {
name: 'Personale',
options: {
theme: 'Tema',
language: 'Lingua',
},
},
},
player: {
playListsText: 'Coda',
openText: 'Apri',
closeText: 'Chiudi',
clickToPlayText: 'Clicca per riprodurre',
clickToPauseText: 'Clicca per mettere in pausa',
nextTrackText: 'Traccia successiva',
previousTrackText: 'Traccia precedente',
volumeText: 'Volume',
toggleMiniModeText: 'Minimizza',
removeAudioListsText: 'Cancella coda',
clickToDeleteText: `Clicca per rimuovere %{name}`,
playModeText: {
order: 'In ordine',
orderLoop: 'Ripeti',
singleLoop: 'Ripeti una volta',
shufflePlay: 'Casuale',
},
},
})

View File

@ -1,86 +0,0 @@
import deepmerge from 'deepmerge'
import englishMessages from 'ra-language-dutch'
export default deepmerge(englishMessages, {
languageName: 'Nederlands',
resources: {
song: {
name: 'Nummer |||| Nummers',
fields: {
albumArtist: 'Album Artiest',
duration: 'Tijd',
trackNumber: 'Nummer #',
playCount: 'Aantal keren afgespeeld',
},
actions: {
addToQueue: 'Toevoegen aan afspeellijst',
},
},
album: {
fields: {
albumArtist: 'Album Artiest',
artist: 'Artiest',
duration: 'Tijd',
songCount: 'Nummerss',
playCount: 'Aantal keren afgespeeld',
},
actions: {
playAll: 'Afspelen',
playNext: 'Hierna afspelen',
addToQueue: 'Toevoegen aan afspeellijst',
shuffle: 'Shuffle',
},
},
},
ra: {
auth: {
welcome1: 'Bedankt voor het installeren van Navidrome!',
welcome2: 'Maak om te beginnen een beheerdersaccount',
confirmPassword: 'Bevestig wachtwoord',
buttonCreateAdmin: 'Beheerder maken',
},
validation: {
invalidChars: 'Gebruik alleen letters en cijfers',
passwordDoesNotMatch: 'Wachtwoord komt niet overeen',
},
},
menu: {
library: 'Bibliotheek',
settings: 'Instellingen',
version: 'Versie %{version}',
theme: 'Thema',
personal: {
name: 'Persoonlijk',
options: {
theme: 'Thema',
language: 'Taal',
},
},
},
player: {
playListsText: 'Afspeellijst afspelen',
openText: 'Openen',
closeText: 'Sluiten',
notContentText: 'Geen muziek',
clickToPlayText: 'Klik om af te spelen',
clickToPauseText: 'Klik om te pauzeren',
nextTrackText: 'Volgende',
previousTrackText: 'Vorige',
reloadText: 'Herladen',
volumeText: 'Volume',
toggleLyricText: 'Songtekst aan/uit',
toggleMiniModeText: 'Minimaliseren',
destroyText: 'Vernietigen',
downloadText: 'Downloaden',
removeAudioListsText: 'Audiolijsten verwijderen',
controllerTitle: '',
clickToDeleteText: `Klik om %{name} te verwijderen`,
emptyLyricText: 'Geen songtekst',
playModeText: {
order: 'In volgorde',
orderLoop: 'Herhalen',
singleLoop: 'Herhaal Eenmalig',
shufflePlay: 'Shuffle',
},
},
})

View File

@ -1,131 +0,0 @@
import deepmerge from 'deepmerge'
import portugueseMessages from 'ra-language-portuguese'
export default deepmerge(portugueseMessages, {
languageName: 'Português',
resources: {
song: {
name: 'Música |||| Músicas',
fields: {
title: 'Título',
artist: 'Artista',
album: 'Álbum',
path: 'Arquivo',
genre: 'Gênero',
compilation: 'Coletânea',
duration: 'Duração',
year: 'Ano',
playCount: 'Execuções',
trackNumber: '#',
size: 'Tamanho',
updatedAt: 'Últ. Atualização',
},
actions: {
playNow: 'Tocar agora',
addToQueue: 'Tocar por último',
},
},
album: {
name: 'Álbum |||| Álbuns',
fields: {
name: 'Nome',
artist: 'Artista',
songCount: 'Músicas',
genre: 'Gênero',
playCount: 'Execuções',
compilation: 'Coletânea',
duration: 'Duração',
year: 'Ano',
},
actions: {
playAll: 'Tocar',
playNext: 'Tocar em seguida',
addToQueue: 'Tocar no fim',
shuffle: 'Aleatório',
},
},
artist: {
name: 'Artista |||| Artistas',
fields: {
name: 'Nome',
albumCount: 'Total de Álbuns',
},
},
user: {
name: 'Usuário |||| Usuários',
fields: {
userName: 'Usuário',
isAdmin: 'Admin?',
lastLoginAt: 'Últ. Login',
updatedAt: 'Últ. Atualização',
name: 'Nome',
},
},
player: {
name: 'Tocador |||| Tocadores',
fields: {
name: 'Nome',
transcodingId: 'Conversão',
maxBitRate: 'Bitrate máx',
client: 'Cliente',
userName: 'Usuário',
lastSeen: 'Últ. acesso',
},
},
transcoding: {
name: 'Conversão |||| Conversões',
fields: {
name: 'Nome',
targetFormat: 'Formato',
defaultBitRate: 'Bitrate padrão',
command: 'Comando',
},
},
},
ra: {
auth: {
welcome1: 'Obrigado por instalar Navidrome!',
welcome2: 'Para iniciar, crie um usuário admin',
confirmPassword: 'Confirme a senha',
buttonCreateAdmin: 'Criar Admin',
},
validation: {
invalidChars: 'Somente use letras e numeros',
passwordDoesNotMatch: 'Senha não confere',
},
page: {
create: 'Criar %{name}',
},
},
menu: {
library: 'Biblioteca',
settings: 'Configurações',
version: 'Versão %{version}',
personal: {
name: 'Pessoal',
options: {
theme: 'Tema',
language: 'Língua',
},
},
},
player: {
playListsText: 'Fila de Execução',
openText: 'Abrir',
closeText: 'Fechar',
clickToPlayText: 'Clique para tocar',
clickToPauseText: 'Clique para pausar',
nextTrackText: 'Próxima faixa',
previousTrackText: 'Faixa anterior',
volumeText: 'Volume',
toggleMiniModeText: 'Minimizar',
removeAudioListsText: 'Limpar fila de execução',
clickToDeleteText: `Clique para remover %{name}`,
playModeText: {
order: 'Em ordem',
orderLoop: 'Repetir tudo',
singleLoop: 'Repetir',
shufflePlay: 'Aleatório',
},
},
})

View File

@ -1,136 +0,0 @@
import deepmerge from 'deepmerge'
import chineseMessages from 'ra-language-chinese'
export default deepmerge(chineseMessages, {
languageName: '简体中文',
resources: {
song: {
name: '歌曲 |||| 曲库',
fields: {
title: '标题',
artist: '歌手',
album: '专辑',
path: '路径',
genre: '类型',
compilation: '收录',
albumArtist: '专辑歌手',
duration: '时长',
year: '年份',
playCount: '播放次数',
trackNumber: '音轨 #',
size: '大小',
updatedAt: '上次更新',
},
actions: {
addToQueue: '稍后播放',
},
},
album: {
name: '专辑 |||| 专辑',
fields: {
name: '名称',
albumArtist: '专辑歌手',
artist: '歌手',
duration: '时长',
songCount: '曲目数',
playCount: '播放次数',
compilation: '合辑',
year: '年份',
},
actions: {
playAll: '播放',
playNext: '播放下一首',
addToQueue: '稍后播放',
shuffle: '刷新',
},
},
artist: {
name: '歌手 |||| 歌手',
fields: {
name: '名称',
albumCount: '歌手数',
},
},
user: {
name: '用户 |||| 用户',
fields: {
userName: '用户名',
isAdmin: '管理员',
lastLoginAt: '最后一次访问',
updatedAt: '上次修改',
name: '名称',
},
},
player: {
name: '用户 |||| 用户',
fields: {
name: '名称',
transcodingId: '转码',
maxBitRate: '最大比特率',
client: '应用程序',
userName: '用户',
lastSeen: '最后一次访问',
},
},
transcoding: {
name: '转码 |||| 转码',
fields: {
name: '名称',
targetFormat: '格式',
defaultBitRate: '默认比特率',
command: '命令',
},
},
},
ra: {
auth: {
welcome1: '感谢您安装Navidrome!',
welcome2: '为了开始使用,请创建一个管理员账户',
confirmPassword: '确认密码',
buttonCreateAdmin: '创建管理员',
},
validation: {
invalidChars: '请只使用字母和数字',
passwordDoesNotMatch: '密码不匹配',
},
},
menu: {
library: '曲库',
settings: '设置',
version: '版本 %{version}',
theme: '主题',
personal: {
name: '个性化',
options: {
theme: '主题',
language: '语言',
},
},
},
player: {
playListsText: '播放队列',
openText: '打开',
closeText: '关闭',
notContentText: '无音乐',
clickToPlayText: '点击播放',
clickToPauseText: '点击暂停',
nextTrackText: '下一首',
previousTrackText: '上一首',
reloadText: 'Reload',
volumeText: '音量',
toggleLyricText: '切换歌词',
toggleMiniModeText: '最小化',
destroyText: '损坏',
downloadText: '下载',
removeAudioListsText: '清空播放列表',
controllerTitle: '',
clickToDeleteText: `点击删除 %{name}`,
emptyLyricText: '无歌词',
playModeText: {
order: '顺序播放',
orderLoop: '列表循环',
singleLoop: '单曲循环',
shufflePlay: '随机播放',
},
},
})

258
ui/src/i18nProvider/en.json Normal file
View File

@ -0,0 +1,258 @@
{
"languageName": "English",
"resources": {
"song": {
"name": "Song |||| Songs",
"fields": {
"albumArtist": "Album Artist",
"duration": "Time",
"trackNumber": "Track #",
"playCount": "Plays",
"title": "Title",
"artist": "Artist",
"album": "Album",
"path": "File path",
"genre": "Genre",
"compilation": "Compilation",
"year": "Year",
"size": "File size",
"updatedAt": "Uploaded at"
},
"actions": {
"addToQueue": "Play Later"
},
"action": {
"playNow": "Play Now"
}
},
"album": {
"name": "Album |||| Albums",
"fields": {
"albumArtist": "Album Artist",
"artist": "Artist",
"duration": "Time",
"songCount": "Songs",
"playCount": "Plays",
"name": "Name",
"genre": "Genre",
"compilation": "Compilation",
"year": "Year"
},
"actions": {
"playAll": "Play",
"playNext": "Play Next",
"addToQueue": "Play Later",
"shuffle": "Shuffle"
}
},
"artist": {
"name": "Artist |||| Artists",
"fields": {
"name": "Name",
"albumCount": "Album Count"
}
},
"user": {
"name": "User |||| Users",
"fields": {
"userName": "Username",
"isAdmin": "Is Admin",
"lastLoginAt": "Last Login At",
"updatedAt": "Updated At",
"name": "Name"
}
},
"player": {
"name": "Player |||| Players",
"fields": {
"name": "Name",
"transcodingId": "Transcoding ID",
"maxBitRate": "Max. Bit Rate",
"client": "Client",
"userName": "Username",
"lastSeen": "Last Seen At"
}
},
"transcoding": {
"name": "Transcoding |||| Transcodings",
"fields": {
"name": "Name",
"targetFormat": "Target Format",
"defaultBitRate": "Default Bit Rate",
"command": "Command"
}
}
},
"ra": {
"auth": {
"welcome1": "Thanks for installing Navidrome!",
"welcome2": "To start, create an admin user",
"confirmPassword": "Confirm Password",
"buttonCreateAdmin": "Create Admin",
"auth_check_error": "Please login to continue",
"user_menu": "Profile",
"username": "Username",
"password": "Password",
"sign_in": "Sign in",
"sign_in_error": "Authentication failed, please retry",
"logout": "Logout"
},
"validation": {
"invalidChars": "Please only use letter and numbers",
"passwordDoesNotMatch": "Password does not match",
"required": "Required",
"minLength": "Must be %{min} characters at least",
"maxLength": "Must be %{max} characters or less",
"minValue": "Must be at least %{min}",
"maxValue": "Must be %{max} or less",
"number": "Must be a number",
"email": "Must be a valid email",
"oneOf": "Must be one of: %{options}",
"regex": "Must match a specific format (regexp): %{pattern}"
},
"action": {
"add_filter": "Add filter",
"add": "Add",
"back": "Go Back",
"bulk_actions": "1 item selected |||| %{smart_count} items selected",
"cancel": "Cancel",
"clear_input_value": "Clear value",
"clone": "Clone",
"confirm": "Confirm",
"create": "Create",
"delete": "Delete",
"edit": "Edit",
"export": "Export",
"list": "List",
"refresh": "Refresh",
"remove_filter": "Remove this filter",
"remove": "Remove",
"save": "Save",
"search": "Search",
"show": "Show",
"sort": "Sort",
"undo": "Undo",
"expand": "Expand",
"close": "Close",
"open_menu": "Open menu",
"close_menu": "Close menu"
},
"boolean": {
"true": "Yes",
"false": "No"
},
"page": {
"create": "Create %{name}",
"dashboard": "Dashboard",
"edit": "%{name} #%{id}",
"error": "Something went wrong",
"list": "%{name}",
"loading": "Loading",
"not_found": "Not Found",
"show": "%{name} #%{id}",
"empty": "No %{name} yet.",
"invite": "Do you want to add one?"
},
"input": {
"file": {
"upload_several": "Drop some files to upload, or click to select one.",
"upload_single": "Drop a file to upload, or click to select it."
},
"image": {
"upload_several": "Drop some pictures to upload, or click to select one.",
"upload_single": "Drop a picture to upload, or click to select it."
},
"references": {
"all_missing": "Unable to find references data.",
"many_missing": "At least one of the associated references no longer appears to be available.",
"single_missing": "Associated reference no longer appears to be available."
},
"password": {
"toggle_visible": "Hide password",
"toggle_hidden": "Show password"
}
},
"message": {
"about": "About",
"are_you_sure": "Are you sure?",
"bulk_delete_content": "Are you sure you want to delete this %{name}? |||| Are you sure you want to delete these %{smart_count} items?",
"bulk_delete_title": "Delete %{name} |||| Delete %{smart_count} %{name}",
"delete_content": "Are you sure you want to delete this item?",
"delete_title": "Delete %{name} #%{id}",
"details": "Details",
"error": "A client error occurred and your request couldn't be completed.",
"invalid_form": "The form is not valid. Please check for errors",
"loading": "The page is loading, just a moment please",
"no": "No",
"not_found": "Either you typed a wrong URL, or you followed a bad link.",
"yes": "Yes",
"unsaved_changes": "Some of your changes weren't saved. Are you sure you want to ignore them?"
},
"navigation": {
"no_results": "No results found",
"no_more_results": "The page number %{page} is out of boundaries. Try the previous page.",
"page_out_of_boundaries": "Page number %{page} out of boundaries",
"page_out_from_end": "Cannot go after last page",
"page_out_from_begin": "Cannot go before page 1",
"page_range_info": "%{offsetBegin}-%{offsetEnd} of %{total}",
"page_rows_per_page": "Rows per page:",
"next": "Next",
"prev": "Prev"
},
"notification": {
"updated": "Element updated |||| %{smart_count} elements updated",
"created": "Element created",
"deleted": "Element deleted |||| %{smart_count} elements deleted",
"bad_item": "Incorrect element",
"item_doesnt_exist": "Element does not exist",
"http_error": "Server communication error",
"data_provider_error": "dataProvider error. Check the console for details.",
"i18n_error": "Cannot load the translations for the specified language",
"canceled": "Action cancelled",
"logged_out": "Your session has ended, please reconnect."
}
},
"message": {
"note": "NOTE",
"transcodingDisabled": "Changing the transcoding configuration through the web interface is disabled for security reasons. If you would like to change (edit or add) transcoding options, restart the server with the %{config} configuration option.",
"transcodingEnabled": "Navidrome is currently running with %{config}, making it possible to run system commands from the transcoding settings using the web interface. We recommend to disable it for security reasons and only enable it when configuring Transcoding options."
},
"menu": {
"library": "Library",
"settings": "Settings",
"version": "Version %{version}",
"theme": "Theme",
"personal": {
"name": "Personal",
"options": {
"theme": "Theme",
"language": "Language"
}
}
},
"player": {
"playListsText": "Play Queue",
"openText": "Open",
"closeText": "Close",
"notContentText": "No music",
"clickToPlayText": "Click to play",
"clickToPauseText": "Click to pause",
"nextTrackText": "Next track",
"previousTrackText": "Previous track",
"reloadText": "Reload",
"volumeText": "Volume",
"toggleLyricText": "Toggle lyric",
"toggleMiniModeText": "Minimize",
"destroyText": "Destroy",
"downloadText": "Download",
"removeAudioListsText": "Delete audio lists",
"clickToDeleteText": "Click to delete %{name}",
"emptyLyricText": "No lyric",
"playModeText": {
"order": "In order",
"orderLoop": "Repeat",
"singleLoop": "Repeat One",
"shufflePlay": "Shuffle"
}
}
}

View File

@ -0,0 +1,28 @@
import polyglotI18nProvider from 'ra-i18n-polyglot'
import dataProvider from '../dataProvider'
import en from './en.json'
const defaultLocale = function () {
const locale = localStorage.getItem('locale')
const current = JSON.parse(localStorage.getItem('translation'))
if (current && current.id === locale) {
return locale
}
return 'en'
}
const i18nProvider = polyglotI18nProvider((locale) => {
if (locale === 'en') {
return en
}
const current = JSON.parse(localStorage.getItem('translation'))
if (current && current.id === locale) {
return JSON.parse(current.data)
}
return dataProvider.getOne('translation', { id: locale }).then((res) => {
localStorage.setItem('translation', JSON.stringify(res.data))
return JSON.parse(res.data.data)
})
}, defaultLocale())
export default i18nProvider

View File

@ -2,18 +2,18 @@ import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Card } from '@material-ui/core'
import {
Title,
SimpleForm,
SelectInput,
useTranslate,
useSetLocale,
SimpleForm,
Title,
useGetList,
useLocale,
useSetLocale,
useTranslate,
} from 'react-admin'
import { makeStyles } from '@material-ui/core/styles'
import HelpOutlineIcon from '@material-ui/icons/HelpOutline'
import { changeTheme } from './actions'
import themes from '../themes'
import i18n from '../i18n'
import { docsUrl } from '../utils/docsUrl'
const useStyles = makeStyles({
@ -35,23 +35,35 @@ const HelpMsg = ({ caption }) => (
)
const SelectLanguage = (props) => {
const { ids, data, loaded } = useGetList(
'translation',
{ page: 1, perPage: -1 },
{ field: '', order: '' },
{}
)
const translate = useTranslate()
const locale = useLocale()
const setLocale = useSetLocale()
const langChoices = Object.keys(i18n).map((key) => {
return { id: key, name: i18n[key].languageName }
})
const locale = useLocale()
const langChoices = [{ id: 'en', name: 'English' }]
if (loaded) {
ids.forEach((id) => langChoices.push({ id: id, name: data[id].name }))
}
langChoices.sort((a, b) => a.name.localeCompare(b.name))
langChoices.push({
id: helpKey,
name: <HelpMsg caption={'Help to translate'} />,
})
return (
<SelectInput
{...props}
source="lamguage"
source="language"
label={translate('menu.personal.options.language')}
defaultValue={locale}
choices={langChoices}
translateChoice={false}
onChange={(event) => {
if (event.target.value === helpKey) {
openInNewTab(docsUrl('/docs/developers/translations/'))
@ -81,6 +93,7 @@ const SelectTheme = (props) => {
source="theme"
label={translate('menu.personal.options.theme')}
defaultValue={currentTheme}
translateChoice={false}
choices={themeChoices}
onChange={(event) => {
if (event.target.value === helpKey) {