diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index dc142c74..1b543b36 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -58,6 +58,17 @@ cat > /usr/share/nginx/html/config.json << EOF } EOF +# Inject OSRM_ENVIRONMENT into index.html meta tag to signal client to load config.json +if [ -f /usr/share/nginx/html/index.html ]; then + TMPFILE=$(mktemp) + awk -v env="$OSRM_ENVIRONMENT" '{ + if ($0 ~ / "$TMPFILE" && mv "$TMPFILE" /usr/share/nginx/html/index.html || true +fi + # Execute the default command (nginx) or any command passed to the container if [ "$#" -eq 0 ]; then exec nginx -g "daemon off;" diff --git a/index.html b/index.html index a7ad8262..f03ad412 100644 --- a/index.html +++ b/index.html @@ -8,6 +8,7 @@ + @@ -59,7 +60,13 @@ }); } - loadConfig().then(loadBundle); + var envMeta = document.querySelector('meta[name="osrm-environment"]'); + if (envMeta && envMeta.content === 'docker') { + loadConfig().then(loadBundle); + } else { + // Not running in Docker — skip fetching config.json to avoid noisy 404s + loadBundle(); + } })(); diff --git a/src/leaflet_options.js b/src/leaflet_options.js index 80a082a2..283cb871 100644 --- a/src/leaflet_options.js +++ b/src/leaflet_options.js @@ -228,11 +228,71 @@ function getZoom() { return parsedZoom; } -// Get language from config +// Get language, prefer browser settings when available; fallback to 'en'. +// Precedence (effective): URL param (handled in index.js) > browser language > 'en' function getLanguage() { - return config.OSRM_LANGUAGE || 'en'; + try { + // Read runtime config each time (honor OSRM_LANGUAGE when set at runtime) + var currentConfig = (typeof window !== 'undefined' ? window.osrmConfig : null) || {}; + var localization = require('./localization'); + var languages = localization.getLanguages(); + + function resolveCandidate(candidate) { + if (!candidate) return undefined; + candidate = String(candidate).trim(); + // exact match (case-sensitive) + if (localization.get(candidate)) return candidate; + + // case-insensitive exact match against available keys (e.g., pt-br -> pt-BR) + var lower = candidate.toLowerCase(); + var keys = Object.keys(languages); + for (var k = 0; k < keys.length; k++) { + if (keys[k].toLowerCase() === lower) return keys[k]; + } + + // primary subtag fallback (e.g., en-US -> en) + var primary = candidate.split(/[-_]/)[0]; + if (!primary) return undefined; + if (localization.get(primary)) return primary; + var lowerPrimary = primary.toLowerCase(); + for (var j = 0; j < keys.length; j++) { + if (keys[j].toLowerCase() === lowerPrimary) return keys[j]; + } + return undefined; + } + + if (currentConfig.OSRM_LANGUAGE) { + var resolved = resolveCandidate(currentConfig.OSRM_LANGUAGE); + return resolved || currentConfig.OSRM_LANGUAGE; + } + + if (typeof window !== 'undefined' && window.navigator) { + var nav = window.navigator; + var candidates = []; + + if (Array.isArray(nav.languages)) { + candidates = candidates.concat(nav.languages); + } + if (nav.language) candidates.push(nav.language); + if (nav.userLanguage) candidates.push(nav.userLanguage); // IE fallback + + for (var i = 0; i < candidates.length; i++) { + var lang = candidates[i]; + if (!lang) continue; + var resolvedLang = resolveCandidate(lang); + if (resolvedLang) return resolvedLang; + } + } + } catch (e) { + // Ignore detection errors and fall back to default + console.warn('Error detecting browser language:', e); + } + + // Fallback to English when no browser language matches + return 'en'; } + // Get default layer from config function getDefaultLayer() { return config.OSRM_DEFAULT_LAYER || 'streets'; diff --git a/test/entrypoint.test.js b/test/entrypoint.test.js index bcd320b5..c83a1913 100644 --- a/test/entrypoint.test.js +++ b/test/entrypoint.test.js @@ -7,7 +7,8 @@ const { execFileSync } = require('child_process'); const entrypointPath = path.join(__dirname, '..', 'docker', 'entrypoint.sh'); -function generateConfig(envOverrides) { +function generateConfig(envOverrides, options) { + options = options || {}; const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'osrm-entrypoint-')); const outputDir = path.join(tempDir, 'usr', 'share', 'nginx', 'html'); const tempEntrypointPath = path.join(tempDir, 'entrypoint.sh'); @@ -19,6 +20,10 @@ function generateConfig(envOverrides) { ); fs.chmodSync(tempEntrypointPath, 0o755); + if (options.indexHtml) { + fs.writeFileSync(path.join(outputDir, 'index.html'), options.indexHtml, 'utf8'); + } + try { execFileSync(tempEntrypointPath, ['true'], { env: { @@ -28,7 +33,19 @@ function generateConfig(envOverrides) { stdio: 'pipe' }); - return JSON.parse(fs.readFileSync(path.join(outputDir, 'config.json'), 'utf8')); + const config = JSON.parse(fs.readFileSync(path.join(outputDir, 'config.json'), 'utf8')); + + if (options.indexHtml) { + let rewritten = null; + try { + rewritten = fs.readFileSync(path.join(outputDir, 'index.html'), 'utf8'); + } catch (e) { + // ignore + } + return { config: config, indexHtml: rewritten }; + } + + return config; } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } diff --git a/test/leaflet_options.test.js b/test/leaflet_options.test.js index d1b28a9b..d8c378ea 100644 --- a/test/leaflet_options.test.js +++ b/test/leaflet_options.test.js @@ -198,20 +198,73 @@ describe('leaflet_options — runtime configuration overrides', () => { }); }); - describe('OSRM_LANGUAGE override', () => { - test('uses custom language when provided', () => { + describe('language precedence (URL > browser > en)', () => { + test('honors OSRM_LANGUAGE runtime override', () => { global.window = { osrmConfig: { OSRM_LANGUAGE: 'de' } }; const leafletOptions = require('../src/leaflet_options'); expect(leafletOptions.defaultState.language).toBe('de'); delete global.window; }); - test('defaults to en when language not provided', () => { - global.window = { osrmConfig: {} }; + test('uses browser language exact match', () => { + global.window = { navigator: { languages: ['de'], language: 'de' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('de'); + delete global.window; + }); + + test('falls back to navigator.language when navigator.languages absent', () => { + global.window = { navigator: { language: 'de' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('de'); + delete global.window; + }); + + test('uses primary subtag when regional locale provided (en-US -> en)', () => { + global.window = { navigator: { languages: ['en-US'], language: 'en-US' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('en'); + delete global.window; + }); + + test('prefers first candidate in navigator.languages array', () => { + global.window = { navigator: { languages: ['fr-CA', 'de'], language: 'fr-CA' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('fr'); + delete global.window; + }); + + test('matches exact regional variant when available (pt-BR)', () => { + global.window = { navigator: { languages: ['pt-BR'], language: 'pt-BR' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('pt-BR'); + delete global.window; + }); + + test('case-insensitive regional tag (pt-br)', () => { + global.window = { navigator: { languages: ['pt-br'], language: 'pt-br' } }; + const leafletOptions = require('../src/leaflet_options'); + expect(leafletOptions.defaultState.language).toBe('pt-BR'); + delete global.window; + }); + + test('falls back to English when no supported browser languages', () => { + global.window = { navigator: { languages: ['xx','yy'], language: 'xx' } }; const leafletOptions = require('../src/leaflet_options'); expect(leafletOptions.defaultState.language).toBe('en'); delete global.window; }); + + test('URL param (hl) takes precedence over browser default when merged', () => { + // Simulate browser default 'en' but URL param asks for 'de' + global.window = { navigator: { languages: ['en'], language: 'en' } }; + const leafletOptions = require('../src/leaflet_options'); + const links = require('../src/links'); + const parsed = links.parse('hl=de'); + const merged = Object.assign({}, leafletOptions.defaultState, parsed); + expect(merged.language).toBe('de'); + delete global.window; + }); }); describe('OSRM_LABEL and OSRM_DEFAULT_LAYER overrides', () => {