Skip to content

Commit be29244

Browse files
DennisOSRMfbarbe00Copilot
authored
feat: partial port of #381. prefer browser language & safe config.json fetch (#440)
* Add translations * fix: cherry pick conflicts * Prefer browser language before runtime config; fallback to English\n\nPrecedence: URL param > browser language > runtime config > English\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make English the default when no URL/browser language is available\n\nPrecedence: URL param > browser language > English\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Only fetch config.json in Docker; avoid noisy 404s\n\nOnly fetch config.json when OSRM_ENVIRONMENT='docker' to avoid noisy 404s in non-Docker environments.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add unit tests for language detection/selection; cover browser navigator cases and URL precedence Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix PR #440 review: honor OSRM_LANGUAGE, case-insensitive locale matching, add tests for language detection and entrypoint meta rewrite Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Fabio Barbero <fbarbe@scarlet.be> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d9a8916 commit be29244

5 files changed

Lines changed: 157 additions & 9 deletions

File tree

docker/entrypoint.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,17 @@ cat > /usr/share/nginx/html/config.json << EOF
5858
}
5959
EOF
6060

61+
# Inject OSRM_ENVIRONMENT into index.html meta tag to signal client to load config.json
62+
if [ -f /usr/share/nginx/html/index.html ]; then
63+
TMPFILE=$(mktemp)
64+
awk -v env="$OSRM_ENVIRONMENT" '{
65+
if ($0 ~ /<meta name="osrm-environment"/) {
66+
sub(/content="[^"]*"/, "content=\"" env "\"")
67+
}
68+
print
69+
}' /usr/share/nginx/html/index.html > "$TMPFILE" && mv "$TMPFILE" /usr/share/nginx/html/index.html || true
70+
fi
71+
6172
# Execute the default command (nginx) or any command passed to the container
6273
if [ "$#" -eq 0 ]; then
6374
exec nginx -g "daemon off;"

index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<link rel='stylesheet' href="css/leaflet.css" />
99
<link rel="stylesheet" href="css/fonts.css" />
1010
<link href='css/site.css' rel='stylesheet' />
11+
<meta name="osrm-environment" content="">
1112

1213
</head>
1314
<body>
@@ -59,7 +60,13 @@
5960
});
6061
}
6162

62-
loadConfig().then(loadBundle);
63+
var envMeta = document.querySelector('meta[name="osrm-environment"]');
64+
if (envMeta && envMeta.content === 'docker') {
65+
loadConfig().then(loadBundle);
66+
} else {
67+
// Not running in Docker — skip fetching config.json to avoid noisy 404s
68+
loadBundle();
69+
}
6370
})();
6471
</script>
6572
</body>

src/leaflet_options.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,11 +228,71 @@ function getZoom() {
228228
return parsedZoom;
229229
}
230230

231-
// Get language from config
231+
// Get language, prefer browser settings when available; fallback to 'en'.
232+
// Precedence (effective): URL param (handled in index.js) > browser language > 'en'
232233
function getLanguage() {
233-
return config.OSRM_LANGUAGE || 'en';
234+
try {
235+
// Read runtime config each time (honor OSRM_LANGUAGE when set at runtime)
236+
var currentConfig = (typeof window !== 'undefined' ? window.osrmConfig : null) || {};
237+
var localization = require('./localization');
238+
var languages = localization.getLanguages();
239+
240+
function resolveCandidate(candidate) {
241+
if (!candidate) return undefined;
242+
candidate = String(candidate).trim();
243+
// exact match (case-sensitive)
244+
if (localization.get(candidate)) return candidate;
245+
246+
// case-insensitive exact match against available keys (e.g., pt-br -> pt-BR)
247+
var lower = candidate.toLowerCase();
248+
var keys = Object.keys(languages);
249+
for (var k = 0; k < keys.length; k++) {
250+
if (keys[k].toLowerCase() === lower) return keys[k];
251+
}
252+
253+
// primary subtag fallback (e.g., en-US -> en)
254+
var primary = candidate.split(/[-_]/)[0];
255+
if (!primary) return undefined;
256+
if (localization.get(primary)) return primary;
257+
var lowerPrimary = primary.toLowerCase();
258+
for (var j = 0; j < keys.length; j++) {
259+
if (keys[j].toLowerCase() === lowerPrimary) return keys[j];
260+
}
261+
return undefined;
262+
}
263+
264+
if (currentConfig.OSRM_LANGUAGE) {
265+
var resolved = resolveCandidate(currentConfig.OSRM_LANGUAGE);
266+
return resolved || currentConfig.OSRM_LANGUAGE;
267+
}
268+
269+
if (typeof window !== 'undefined' && window.navigator) {
270+
var nav = window.navigator;
271+
var candidates = [];
272+
273+
if (Array.isArray(nav.languages)) {
274+
candidates = candidates.concat(nav.languages);
275+
}
276+
if (nav.language) candidates.push(nav.language);
277+
if (nav.userLanguage) candidates.push(nav.userLanguage); // IE fallback
278+
279+
for (var i = 0; i < candidates.length; i++) {
280+
var lang = candidates[i];
281+
if (!lang) continue;
282+
var resolvedLang = resolveCandidate(lang);
283+
if (resolvedLang) return resolvedLang;
284+
}
285+
}
286+
} catch (e) {
287+
// Ignore detection errors and fall back to default
288+
console.warn('Error detecting browser language:', e);
289+
}
290+
291+
// Fallback to English when no browser language matches
292+
return 'en';
234293
}
235294

295+
236296
// Get default layer from config
237297
function getDefaultLayer() {
238298
return config.OSRM_DEFAULT_LAYER || 'streets';

test/entrypoint.test.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const { execFileSync } = require('child_process');
77

88
const entrypointPath = path.join(__dirname, '..', 'docker', 'entrypoint.sh');
99

10-
function generateConfig(envOverrides) {
10+
function generateConfig(envOverrides, options) {
11+
options = options || {};
1112
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'osrm-entrypoint-'));
1213
const outputDir = path.join(tempDir, 'usr', 'share', 'nginx', 'html');
1314
const tempEntrypointPath = path.join(tempDir, 'entrypoint.sh');
@@ -19,6 +20,10 @@ function generateConfig(envOverrides) {
1920
);
2021
fs.chmodSync(tempEntrypointPath, 0o755);
2122

23+
if (options.indexHtml) {
24+
fs.writeFileSync(path.join(outputDir, 'index.html'), options.indexHtml, 'utf8');
25+
}
26+
2227
try {
2328
execFileSync(tempEntrypointPath, ['true'], {
2429
env: {
@@ -28,7 +33,19 @@ function generateConfig(envOverrides) {
2833
stdio: 'pipe'
2934
});
3035

31-
return JSON.parse(fs.readFileSync(path.join(outputDir, 'config.json'), 'utf8'));
36+
const config = JSON.parse(fs.readFileSync(path.join(outputDir, 'config.json'), 'utf8'));
37+
38+
if (options.indexHtml) {
39+
let rewritten = null;
40+
try {
41+
rewritten = fs.readFileSync(path.join(outputDir, 'index.html'), 'utf8');
42+
} catch (e) {
43+
// ignore
44+
}
45+
return { config: config, indexHtml: rewritten };
46+
}
47+
48+
return config;
3249
} finally {
3350
fs.rmSync(tempDir, { recursive: true, force: true });
3451
}

test/leaflet_options.test.js

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,20 +198,73 @@ describe('leaflet_options — runtime configuration overrides', () => {
198198
});
199199
});
200200

201-
describe('OSRM_LANGUAGE override', () => {
202-
test('uses custom language when provided', () => {
201+
describe('language precedence (URL > browser > en)', () => {
202+
test('honors OSRM_LANGUAGE runtime override', () => {
203203
global.window = { osrmConfig: { OSRM_LANGUAGE: 'de' } };
204204
const leafletOptions = require('../src/leaflet_options');
205205
expect(leafletOptions.defaultState.language).toBe('de');
206206
delete global.window;
207207
});
208208

209-
test('defaults to en when language not provided', () => {
210-
global.window = { osrmConfig: {} };
209+
test('uses browser language exact match', () => {
210+
global.window = { navigator: { languages: ['de'], language: 'de' } };
211+
const leafletOptions = require('../src/leaflet_options');
212+
expect(leafletOptions.defaultState.language).toBe('de');
213+
delete global.window;
214+
});
215+
216+
test('falls back to navigator.language when navigator.languages absent', () => {
217+
global.window = { navigator: { language: 'de' } };
218+
const leafletOptions = require('../src/leaflet_options');
219+
expect(leafletOptions.defaultState.language).toBe('de');
220+
delete global.window;
221+
});
222+
223+
test('uses primary subtag when regional locale provided (en-US -> en)', () => {
224+
global.window = { navigator: { languages: ['en-US'], language: 'en-US' } };
225+
const leafletOptions = require('../src/leaflet_options');
226+
expect(leafletOptions.defaultState.language).toBe('en');
227+
delete global.window;
228+
});
229+
230+
test('prefers first candidate in navigator.languages array', () => {
231+
global.window = { navigator: { languages: ['fr-CA', 'de'], language: 'fr-CA' } };
232+
const leafletOptions = require('../src/leaflet_options');
233+
expect(leafletOptions.defaultState.language).toBe('fr');
234+
delete global.window;
235+
});
236+
237+
test('matches exact regional variant when available (pt-BR)', () => {
238+
global.window = { navigator: { languages: ['pt-BR'], language: 'pt-BR' } };
239+
const leafletOptions = require('../src/leaflet_options');
240+
expect(leafletOptions.defaultState.language).toBe('pt-BR');
241+
delete global.window;
242+
});
243+
244+
test('case-insensitive regional tag (pt-br)', () => {
245+
global.window = { navigator: { languages: ['pt-br'], language: 'pt-br' } };
246+
const leafletOptions = require('../src/leaflet_options');
247+
expect(leafletOptions.defaultState.language).toBe('pt-BR');
248+
delete global.window;
249+
});
250+
251+
test('falls back to English when no supported browser languages', () => {
252+
global.window = { navigator: { languages: ['xx','yy'], language: 'xx' } };
211253
const leafletOptions = require('../src/leaflet_options');
212254
expect(leafletOptions.defaultState.language).toBe('en');
213255
delete global.window;
214256
});
257+
258+
test('URL param (hl) takes precedence over browser default when merged', () => {
259+
// Simulate browser default 'en' but URL param asks for 'de'
260+
global.window = { navigator: { languages: ['en'], language: 'en' } };
261+
const leafletOptions = require('../src/leaflet_options');
262+
const links = require('../src/links');
263+
const parsed = links.parse('hl=de');
264+
const merged = Object.assign({}, leafletOptions.defaultState, parsed);
265+
expect(merged.language).toBe('de');
266+
delete global.window;
267+
});
215268
});
216269

217270
describe('OSRM_LABEL and OSRM_DEFAULT_LAYER overrides', () => {

0 commit comments

Comments
 (0)